diff --git a/libraries/WebServer/examples/ChunkWriting/ChunkWriting.ino b/libraries/WebServer/examples/ChunkWriting/ChunkWriting.ino new file mode 100644 index 00000000000..50c29322302 --- /dev/null +++ b/libraries/WebServer/examples/ChunkWriting/ChunkWriting.ino @@ -0,0 +1,75 @@ +/* + * This example demonstrates how to send an HTTP response using chunks + * It will create an HTTP Server (port 80) associated with an a MDNS service + * Access the HTTP server using a Web Browser: + * URL can be composed using the MDNS name "esp32_chunk_resp.local" + * http://esp32_chunk_resp.local/ + * or the IP Address that will be printed out, such as for instance 192.168.1.10 + * http://192.168.1.10/ + * + * ESP32 Server response can also be viewed using the curl command: + * curl -i esp32_chunk_resp.local:80 + * curl -i --raw esp32_chunk_resp.local:80 + */ + +#include +#include +#include +#include + +const char *ssid = "........"; +const char *password = "........"; + +WebServer server(80); + +void handleChunks() { + uint8_t countDown = 10; + server.chunkResponseBegin(); + char countContent[8]; + while (countDown) { + sprintf(countContent, "%d...\r\n", countDown--); + server.chunkWrite(countContent, strlen(countContent)); + // count down shall show up in the browser only after about 5 seconds when finishing the whole transmission + // using "curl -i esp32_chunk_resp.local:80", it will show the count down as it sends each chunk + delay(500); + } + server.chunkWrite("DONE!", 5); + server.chunkResponseEnd(); +} + +void setup(void) { + Serial.begin(115200); + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + Serial.println(""); + + // Wait for connection + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println(""); + Serial.print("Connected to "); + Serial.println(ssid); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + + // Use the URL: http://esp32_chunk_resp.local/ + if (MDNS.begin("esp32_chunk_resp")) { + Serial.println("MDNS responder started"); + } + + server.on("/", handleChunks); + + server.onNotFound([]() { + server.send(404, "text/plain", "Page not found"); + }); + + server.begin(); + Serial.println("HTTP server started"); +} + +void loop(void) { + server.handleClient(); + delay(2); //allow the cpu to switch to other tasks +} diff --git a/libraries/WebServer/examples/ChunkWriting/ci.json b/libraries/WebServer/examples/ChunkWriting/ci.json new file mode 100644 index 00000000000..618e46bd244 --- /dev/null +++ b/libraries/WebServer/examples/ChunkWriting/ci.json @@ -0,0 +1,6 @@ +{ + "requires_any": [ + "CONFIG_SOC_WIFI_SUPPORTED=y", + "CONFIG_ESP_WIFI_REMOTE_ENABLED=y" + ] +} diff --git a/libraries/WebServer/src/WebServer.cpp b/libraries/WebServer/src/WebServer.cpp index 7523e40259b..e67fcec05e4 100644 --- a/libraries/WebServer/src/WebServer.cpp +++ b/libraries/WebServer/src/WebServer.cpp @@ -551,6 +551,76 @@ void WebServer::enableETag(bool enable, ETagFunction fn) { _eTagFunction = fn; } +void WebServer::chunkResponseBegin(const char *contentType) { + if (_chunkedResponseActive) { + log_e("Already in chunked response mode"); + return; + } + + if (strchr(contentType, '\r') || strchr(contentType, '\n')) { + log_e("Invalid character in content type"); + return; + } + + _chunkedResponseActive = true; + _chunkedClient = _currentClient; + + _contentLength = CONTENT_LENGTH_UNKNOWN; + + String header; + _prepareHeader(header, 200, contentType, 0); + _currentClientWrite(header.c_str(), header.length()); + + _chunkedResponseActive = true; + _chunkedClient = _currentClient; +} + +void WebServer::chunkWrite(const char *data, size_t length) { + if (!_chunkedResponseActive) { + log_e("Chunked response has not been started"); + return; + } + + char chunkSize[11]; + snprintf(chunkSize, sizeof(chunkSize), "%zx\r\n", length); + + if (_chunkedClient.write(chunkSize) != strlen(chunkSize)) { + log_e("Failed to write chunk size"); + _chunkedResponseActive = false; + return; + } + + if (_chunkedClient.write((const uint8_t *)data, length) != length) { + log_e("Failed to write chunk data"); + _chunkedResponseActive = false; + return; + } + + if (_chunkedClient.write("\r\n") != 2) { + log_e("Failed to write chunk terminator"); + _chunkedResponseActive = false; + return; + } +} + +void WebServer::chunkResponseEnd() { + if (!_chunkedResponseActive) { + log_e("Chunked response has not been started"); + return; + } + + if (_chunkedClient.write("0\r\n\r\n", 5) != 5) { + log_e("Failed to write terminating chunk"); + } + + _chunkedClient.flush(); + _chunkedResponseActive = false; + _chunked = false; + _chunkedClient = NetworkClient(); + + _clearResponseHeaders(); +} + void WebServer::_prepareHeader(String &response, int code, const char *content_type, size_t contentLength) { _responseCode = code; diff --git a/libraries/WebServer/src/WebServer.h b/libraries/WebServer/src/WebServer.h index 8daf12c5c30..498bcb5806c 100644 --- a/libraries/WebServer/src/WebServer.h +++ b/libraries/WebServer/src/WebServer.h @@ -115,6 +115,10 @@ class WebServer { const String AuthTypeDigest = F("Digest"); const String AuthTypeBasic = F("Basic"); + void chunkResponseBegin(const char *contentType = "text/plain"); + void chunkWrite(const char *data, size_t length); + void chunkResponseEnd(); + /* Callbackhandler for authentication. The extra parameters depend on the * HTTPAuthMethod mode: * @@ -241,6 +245,10 @@ class WebServer { static String responseCodeToString(int code); +private: + bool _chunkedResponseActive = false; + NetworkClient _chunkedClient; // Store by value, no dangling pointer + protected: virtual size_t _currentClientWrite(const char *b, size_t l) { return _currentClient.write(b, l);