forked from me-no-dev/ESPAsyncWebServer
-
Notifications
You must be signed in to change notification settings - Fork 90
Support chunked encoding in requests #377
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MitchBradley
wants to merge
4
commits into
ESP32Async:main
Choose a base branch
from
MitchBradley:ChunkedRequest
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+358
−3
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
32e0df6
Support chunked encoding in requests
MitchBradley 4b24f82
Implemented suggestions from Copilot
MitchBradley 7bf8437
More tweaks from additional Copilot review
MitchBradley 5ebde96
Allocate _tempObject in a way that permits automatic deallocation
MitchBradley File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| // SPDX-License-Identifier: LGPL-3.0-or-later | ||
| // Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Mitch Bradley | ||
|
|
||
| // | ||
| // - Test for chunked encoding in requests | ||
| // | ||
|
|
||
| #include <Arduino.h> | ||
| #if defined(ESP32) || defined(LIBRETINY) | ||
| #include <AsyncTCP.h> | ||
| #include <WiFi.h> | ||
| #elif defined(ESP8266) | ||
| #include <ESP8266WiFi.h> | ||
| #include <ESPAsyncTCP.h> | ||
| #elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) | ||
| #include <RPAsyncTCP.h> | ||
| #include <WiFi.h> | ||
| #endif | ||
|
|
||
| #include <ESPAsyncWebServer.h> | ||
| #include <LittleFS.h> | ||
|
|
||
| using namespace asyncsrv; | ||
|
|
||
| // Tests: | ||
| // | ||
| // Upload a file with PUT | ||
| // curl -T myfile.txt http://192.168.4.1/ | ||
| // | ||
| // Upload a file with PUT using chunked encoding | ||
| // curl -T bigfile.txt -H 'Transfer-Encoding: chunked' http://192.168.4.1/ | ||
| // ** Note: If the file will not fit in the available space, the server | ||
| // ** does not know that in advance due to the lack of a Content-Length header. | ||
| // ** The transfer will proceed until the filesystem fills up, then the transfer | ||
| // ** will fail and the partial file will be deleted. This works correctly with | ||
| // ** recent versions (e.g. pioarduino) of the arduinoespressif32 framework, but | ||
| // ** fails with the stale 3.20017.241212+sha.dcc1105b version due to a LittleFS | ||
| // ** bug that has since been fixed. | ||
| // | ||
| // Immediately reject a chunked PUT that will not fit in available space | ||
| // curl -T bigfile.txt -H 'Transfer-Encoding: chunked' -H 'X-Expected-Entity-Length: 99999999' http://192.168.4.1/ | ||
| // ** Note: MacOS WebDAVFS supplies the X-Expected-Entity-Length header with its | ||
| // ** chunked PUTs | ||
|
|
||
| // This struct is used with _tempObject to communicate between handleBody and a subsequent handleRequest | ||
| struct RequestState { | ||
| File outFile; | ||
| }; | ||
|
|
||
| void handleRequest(AsyncWebServerRequest *request) { | ||
| Serial.print(request->methodToString()); | ||
| Serial.print(" "); | ||
| Serial.println(request->url()); | ||
|
|
||
| if (request->method() != HTTP_PUT) { | ||
| request->send(400); // Bad Request | ||
| return; | ||
| } | ||
|
|
||
| // If request->_tempObject is not null, handleBody already | ||
| // did the necessary work for a PUT operation. Otherwise, | ||
| // handleBody was either not called, or did nothing, so we | ||
| // handle the request later in this routine. That happens | ||
| // when a non-chunked PUT has Content-Length: 0. | ||
| auto state = static_cast<RequestState *>(request->_tempObject); | ||
| if (state) { | ||
| // If handleBody successfully opened the file, whether or not it | ||
| // wrote data to it, we close it here and send the "created" | ||
| // response. If handleBody did not open the file, because the | ||
| // open attempt failed or because the operation was rejected, | ||
| // state will be non-null but state->outFile will be false. In | ||
| // that case, handleBody has already sent an appropriate | ||
| // response code. | ||
|
|
||
| if (state->outFile) { | ||
| // The file was already opened and written in handleBody so | ||
| // we close it here and issue the appropriate response. | ||
| state->outFile.close(); | ||
| request->send(201); // Created | ||
| } | ||
| // The resources used by state will be automatically freed | ||
| // when the framework frees the _tempObject pointer | ||
| return; | ||
| } | ||
|
|
||
| String path = request->url(); | ||
|
|
||
| // This PUT code executes if the body was empty, which | ||
| // can happen if the client creates a zero-length file. | ||
| // MacOS WebDAVFS does that, then later LOCKs the file | ||
| // and issues a subsequent PUT with body contents. | ||
|
|
||
| #ifdef ESP32 | ||
| File file = LittleFS.open(path, "w", true); | ||
| #else | ||
| File file = LittleFS.open(path, "w"); | ||
| #endif | ||
|
|
||
| if (file) { | ||
| file.close(); | ||
| request->send(201); // Created | ||
| return; | ||
| } | ||
| request->send(403); | ||
| } | ||
|
|
||
| void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { | ||
| if (request->method() == HTTP_PUT) { | ||
| auto state = static_cast<RequestState *>(request->_tempObject); | ||
| if (index == 0) { | ||
| // parse the url to a proper path | ||
| String path = request->url(); | ||
|
|
||
| // Allocate the _tempObject memory | ||
| request->_tempObject = std::malloc(sizeof(RequestState)); | ||
|
|
||
| // Use placement new to construct the RequestState object therein | ||
| RequestState *state = new (request->_tempObject) RequestState{File()}; | ||
|
|
||
| if (total) { | ||
| #ifdef ESP32 | ||
| size_t avail = LittleFS.totalBytes() - LittleFS.usedBytes(); | ||
| #else | ||
| FSInfo info; | ||
| LittleFS.info(info); | ||
| auto avail = info.totalBytes - info.usedBytes; | ||
| #endif | ||
| avail -= 4096; // Reserve a block for overhead | ||
| if (total > avail) { | ||
| Serial.printf("PUT %u bytes will not fit in available space (%u).\n", total, avail); | ||
| request->send(507, "text/plain", "Too large for available storage\r\n"); | ||
| return; | ||
| } | ||
| } | ||
| Serial.print("Opening "); | ||
| Serial.print(path); | ||
| Serial.println(" from handleBody"); | ||
| #ifdef ESP32 | ||
| File file = LittleFS.open(path, "w", true); | ||
| #else | ||
| File file = LittleFS.open(path, "w"); | ||
| #endif | ||
| if (!file) { | ||
| request->send(500, "text/plain", "Cannot create the file"); | ||
| return; | ||
| } | ||
| if (file.isDirectory()) { | ||
| file.close(); | ||
| Serial.println("Cannot PUT to a directory"); | ||
| request->send(403, "text/plain", "Cannot PUT to a directory"); | ||
| return; | ||
| } | ||
| // If we already returned, the File object in | ||
| // request->_tempObject is the default-constructed one. The | ||
| // presence of a non-default-constructed File in state->outFile | ||
| // indicates that the file was opened successfully and is ready | ||
| // to receive body data. The File will be closed later when | ||
| // handleRequest is called after all calls to handleBody | ||
|
|
||
| std::swap(state->outFile, file); | ||
| // Now request->_tempObject contains the actual file object which owns it, | ||
| // and default-constructed File() object is in file, which will | ||
| // go out of scope | ||
| } | ||
| if (state && state->outFile) { | ||
| Serial.printf("write %d at %d\n", len, index); | ||
| auto actual = state->outFile.write(data, len); | ||
| if (actual != len) { | ||
| Serial.println("WebDAV write failed. Deleting file."); | ||
|
|
||
| // Replace the File object in state with a null one | ||
| File file{}; | ||
| std::swap(state->outFile, file); | ||
| file.close(); | ||
|
|
||
| String path = request->url(); | ||
| LittleFS.remove(path); | ||
| request->send(507, "text/plain", "Too large for available storage\r\n"); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| static AsyncWebServer server(80); | ||
|
|
||
| void setup() { | ||
| Serial.begin(115200); | ||
|
|
||
| #if ASYNCWEBSERVER_WIFI_SUPPORTED | ||
| #define AP_SUBNET 100 | ||
| IPAddress local_IP(192, 168, AP_SUBNET, 1); | ||
| IPAddress gateway(192, 168, AP_SUBNET, 1); | ||
| IPAddress subnet(255, 255, 255, 0); | ||
| WiFi.softAPConfig(local_IP, gateway, subnet); | ||
|
|
||
| WiFi.mode(WIFI_AP); | ||
| WiFi.softAP("esp-captive"); | ||
| #endif | ||
|
|
||
| #ifdef ESP32 | ||
| LittleFS.begin(true); | ||
| #else | ||
| LittleFS.begin(); | ||
| #endif | ||
|
|
||
| server.onRequestBody(handleBody); | ||
| server.onNotFound(handleRequest); | ||
|
|
||
| server.begin(); | ||
| } | ||
|
|
||
| void loop() { | ||
| delay(100); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.