Skip to content

Commit 0d10b08

Browse files
authored
Merge pull request #377 from MitchBradley/ChunkedRequest
Support chunked encoding in requests
2 parents f2b8a99 + 076b5b7 commit 0d10b08

7 files changed

Lines changed: 417 additions & 3 deletions

File tree

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// SPDX-License-Identifier: LGPL-3.0-or-later
2+
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Mitch Bradley
3+
4+
//
5+
// - Test for chunked encoding in requests
6+
//
7+
8+
#include <Arduino.h>
9+
#if defined(ESP32) || defined(LIBRETINY)
10+
#include <AsyncTCP.h>
11+
#include <WiFi.h>
12+
#elif defined(ESP8266)
13+
#include <ESP8266WiFi.h>
14+
#include <ESPAsyncTCP.h>
15+
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
16+
#include <RPAsyncTCP.h>
17+
#include <WiFi.h>
18+
#endif
19+
20+
#include <ESPAsyncWebServer.h>
21+
#include <LittleFS.h>
22+
23+
using namespace asyncsrv;
24+
25+
// Tests:
26+
//
27+
// Upload a file with PUT
28+
// curl -T myfile.txt http://192.168.4.1/
29+
//
30+
// Upload a file with PUT using chunked encoding
31+
// curl -T bigfile.txt -H 'Transfer-Encoding: chunked' http://192.168.4.1/
32+
// ** Note: If the file will not fit in the available space, the server
33+
// ** does not know that in advance due to the lack of a Content-Length header.
34+
// ** The transfer will proceed until the filesystem fills up, then the transfer
35+
// ** will fail and the partial file will be deleted. This works correctly with
36+
// ** recent versions (e.g. pioarduino) of the arduinoespressif32 framework, but
37+
// ** fails with the stale 3.20017.241212+sha.dcc1105b version due to a LittleFS
38+
// ** bug that has since been fixed.
39+
//
40+
// Immediately reject a chunked PUT that will not fit in available space
41+
// curl -T bigfile.txt -H 'Transfer-Encoding: chunked' -H 'X-Expected-Entity-Length: 99999999' http://192.168.4.1/
42+
// ** Note: MacOS WebDAVFS supplies the X-Expected-Entity-Length header with its
43+
// ** chunked PUTs
44+
// Malformed chunk (triggers abort)
45+
// printf 'PUT /bad HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\n12345\r\nZ\r\n' | nc 192.168.4.1 80
46+
47+
// This struct is used with _tempObject to communicate between handleBody and a subsequent handleRequest
48+
struct RequestState {
49+
File outFile;
50+
};
51+
52+
void handleRequest(AsyncWebServerRequest *request) {
53+
Serial.print(request->methodToString());
54+
Serial.print(" ");
55+
Serial.println(request->url());
56+
57+
if (request->method() != HTTP_PUT) {
58+
request->send(400); // Bad Request
59+
return;
60+
}
61+
62+
// If request->_tempObject is not null, handleBody already
63+
// did the necessary work for a PUT operation. Otherwise,
64+
// handleBody was either not called, or did nothing, so we
65+
// handle the request later in this routine. That happens
66+
// when a non-chunked PUT has Content-Length: 0.
67+
auto state = static_cast<RequestState *>(request->_tempObject);
68+
if (state) {
69+
// If handleBody successfully opened the file, whether or not it
70+
// wrote data to it, we close it here and send the "created"
71+
// response. If handleBody did not open the file, because the
72+
// open attempt failed or because the operation was rejected,
73+
// state will be non-null but state->outFile will be false. In
74+
// that case, handleBody has already sent an appropriate
75+
// response code.
76+
77+
if (state->outFile) {
78+
// The file was already opened and written in handleBody so
79+
// we close it here and issue the appropriate response.
80+
state->outFile.close();
81+
request->send(201); // Created
82+
}
83+
// The resources used by state will be automatically freed
84+
// when the framework frees the _tempObject pointer
85+
return;
86+
}
87+
88+
String path = request->url();
89+
90+
// This PUT code executes if the body was empty, which
91+
// can happen if the client creates a zero-length file.
92+
// MacOS WebDAVFS does that, then later LOCKs the file
93+
// and issues a subsequent PUT with body contents.
94+
95+
#ifdef ESP32
96+
File file = LittleFS.open(path, "w", true);
97+
#else
98+
File file = LittleFS.open(path, "w");
99+
#endif
100+
101+
if (file) {
102+
file.close();
103+
request->send(201); // Created
104+
return;
105+
}
106+
request->send(403);
107+
}
108+
109+
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
110+
if (request->method() == HTTP_PUT) {
111+
auto state = static_cast<RequestState *>(request->_tempObject);
112+
if (index == 0) {
113+
// parse the url to a proper path
114+
String path = request->url();
115+
116+
// Allocate the _tempObject memory
117+
request->_tempObject = std::malloc(sizeof(RequestState));
118+
119+
// Use placement new to construct the RequestState object therein
120+
state = new (request->_tempObject) RequestState{File()};
121+
122+
// If the client disconnects or there is a parsing error,
123+
// handleRequest will not be called so we need to close
124+
// the file. The memory backing _tempObject will be freed
125+
// automatically.
126+
request->onDisconnect([request]() {
127+
Serial.println("Client disconnected");
128+
auto state = static_cast<RequestState *>(request->_tempObject);
129+
if (state) {
130+
if (state->outFile) {
131+
state->outFile.close();
132+
}
133+
}
134+
});
135+
136+
if (total) {
137+
#ifdef ESP32
138+
size_t avail = LittleFS.totalBytes() - LittleFS.usedBytes();
139+
#else
140+
FSInfo info;
141+
LittleFS.info(info);
142+
auto avail = info.totalBytes - info.usedBytes;
143+
#endif
144+
avail = (avail >= 4096) ? avail - 4096 : avail; // Reserve a block for overhead
145+
if (total > avail) {
146+
Serial.printf("PUT %zu bytes will not fit in available space (%zu).\n", total, avail);
147+
request->send(507, "text/plain", "Too large for available storage\r\n");
148+
return;
149+
}
150+
}
151+
Serial.print("Opening ");
152+
Serial.print(path);
153+
Serial.println(" from handleBody");
154+
#ifdef ESP32
155+
File file = LittleFS.open(path, "w", true);
156+
#else
157+
File file = LittleFS.open(path, "w");
158+
#endif
159+
if (!file) {
160+
request->send(500, "text/plain", "Cannot create the file");
161+
return;
162+
}
163+
if (file.isDirectory()) {
164+
file.close();
165+
Serial.println("Cannot PUT to a directory");
166+
request->send(403, "text/plain", "Cannot PUT to a directory");
167+
return;
168+
}
169+
// If we already returned, the File object in
170+
// request->_tempObject is the default-constructed one. The
171+
// presence of a non-default-constructed File in state->outFile
172+
// indicates that the file was opened successfully and is ready
173+
// to receive body data. The File will be closed later when
174+
// handleRequest is called after all calls to handleBody
175+
176+
std::swap(state->outFile, file);
177+
// Now request->_tempObject contains the actual file object which owns it,
178+
// and default-constructed File() object is in file, which will
179+
// go out of scope
180+
}
181+
if (state && state->outFile) {
182+
Serial.printf("Writing %zu bytes at offset %zu\n", len, index);
183+
auto actual = state->outFile.write(data, len);
184+
if (actual != len) {
185+
Serial.println("WebDAV write failed. Deleting file.");
186+
187+
// Replace the File object in state with a null one
188+
File file{};
189+
std::swap(state->outFile, file);
190+
file.close();
191+
192+
String path = request->url();
193+
LittleFS.remove(path);
194+
request->send(507, "text/plain", "Too large for available storage\r\n");
195+
return;
196+
}
197+
}
198+
}
199+
}
200+
201+
static AsyncWebServer server(80);
202+
203+
void setup() {
204+
Serial.begin(115200);
205+
206+
#if ASYNCWEBSERVER_WIFI_SUPPORTED
207+
WiFi.mode(WIFI_AP);
208+
WiFi.softAP("esp-captive");
209+
#endif
210+
211+
#ifdef ESP32
212+
LittleFS.begin(true);
213+
#else
214+
LittleFS.begin();
215+
#endif
216+
217+
server.onRequestBody(handleBody);
218+
server.onNotFound(handleRequest);
219+
220+
server.begin();
221+
}
222+
223+
void loop() {
224+
delay(100);
225+
}

platformio.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ lib_dir = .
66
; src_dir = examples/Auth
77
; src_dir = examples/CaptivePortal
88
; src_dir = examples/CatchAllHandler
9+
; src_dir = examples/ChunkRequest
910
; src_dir = examples/ChunkResponse
1011
; src_dir = examples/ChunkRetryResponse
1112
; src_dir = examples/CORS

src/AsyncJson.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,17 @@ void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uin
200200
}
201201

202202
if (index == 0) {
203+
if (total == 0) {
204+
// If total is 0, it is probably a chunked request without an
205+
// X-Expected-Entity-Length header. In that case there is
206+
// no way to know the actual length in advance. The best
207+
// way to handle this would be to use a String instead of
208+
// a fixed-length buffer, but for now we just reject.
209+
async_ws_log_e("AsyncJson cannot handle chunked requests without X-Expected-Entity-Length");
210+
request->abort();
211+
return;
212+
}
213+
203214
// this check allows request->_tempObject to be initialized from a middleware
204215
if (request->_tempObject == NULL) {
205216
request->_tempObject = calloc(total + 1, sizeof(uint8_t)); // null-terminated string

src/ESPAsyncWebServer.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,13 @@ class AsyncWebServerRequest {
288288
size_t _itemBufferIndex;
289289
bool _itemIsFile;
290290

291+
size_t _chunkStartIndex; // Offset from start of the chunked data stream
292+
size_t _chunkOffset; // Offset into the current chunk
293+
size_t _chunkSize; // Size of the current chunk
294+
uint8_t _chunkedParseState;
295+
uint8_t _chunkedLastChar;
296+
bool _parseChunkedBytes(uint8_t *data, size_t len);
297+
291298
void _onPoll();
292299
void _onAck(size_t len, uint32_t time);
293300
void _onError(int8_t error);

0 commit comments

Comments
 (0)