Skip to content

Commit c127402

Browse files
authored
Merge pull request #329 from ESP32Async/upload
Fix multipart file upload handling and improve error responses
2 parents 37933e3 + 2034552 commit c127402

File tree

4 files changed

+177
-0
lines changed

4 files changed

+177
-0
lines changed

examples/Upload/Upload.ino

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ void setup() {
6363
if (!buffer->reserve(size)) {
6464
delete buffer;
6565
request->abort();
66+
return;
6667
}
6768
request->_tempObject = buffer;
6869
}
@@ -100,6 +101,7 @@ void setup() {
100101

101102
if (!request->_tempFile) {
102103
request->send(400, "text/plain", "File not available for writing");
104+
return;
103105
}
104106
}
105107
if (len) {
@@ -141,6 +143,7 @@ void setup() {
141143

142144
// first pass ?
143145
if (!index) {
146+
// Note: using content type to determine size is not reliable!
144147
size_t size = request->header("Content-Length").toInt();
145148
if (!size) {
146149
request->send(400, "text/plain", "No Content-Length");
@@ -150,6 +153,7 @@ void setup() {
150153
if (!buffer) {
151154
// not enough memory
152155
request->abort();
156+
return;
153157
} else {
154158
request->_tempObject = buffer;
155159
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// SPDX-License-Identifier: LGPL-3.0-or-later
2+
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
3+
4+
//
5+
// Demo to upload a firmware and filesystem image via multipart form data
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 <StreamString.h>
22+
#include <LittleFS.h>
23+
24+
// ESP32 example ONLY
25+
#ifdef ESP32
26+
#include <Update.h>
27+
#endif
28+
29+
static AsyncWebServer server(80);
30+
31+
void setup() {
32+
Serial.begin(115200);
33+
34+
if (!LittleFS.begin()) {
35+
LittleFS.format();
36+
LittleFS.begin();
37+
}
38+
39+
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI || CONFIG_ESP32_WIFI_ENABLED
40+
WiFi.mode(WIFI_AP);
41+
WiFi.softAP("esp-captive");
42+
#endif
43+
44+
// ESP32 example ONLY
45+
#ifdef ESP32
46+
47+
// Shows how to get the fw and fs (names) and filenames from a multipart upload,
48+
// and also how to handle multiple file uploads in a single request.
49+
//
50+
// This example also shows how to pass and handle different parameters having the same name in query string, post form and content-disposition.
51+
//
52+
// Execute in the terminal, in order:
53+
//
54+
// 1. Build firmware: pio run -e arduino-3
55+
// 2. Build FS image: pio run -e arduino-3 -t buildfs
56+
// 3. Flash both at the same time: curl -v -F "name=Bob" -F "[email protected]/build/arduino-3/firmware.bin" -F "[email protected]/build/arduino-3/littlefs.bin" http://192.168.4.1/flash?name=Bill
57+
//
58+
server.on(
59+
"/flash", HTTP_POST,
60+
[](AsyncWebServerRequest *request) {
61+
if (request->getResponse()) {
62+
// response already created
63+
return;
64+
}
65+
66+
// list all parameters
67+
Serial.println("Request parameters:");
68+
const size_t params = request->params();
69+
for (size_t i = 0; i < params; i++) {
70+
const AsyncWebParameter *p = request->getParam(i);
71+
Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size());
72+
}
73+
74+
Serial.println("Flash / Filesystem upload completed");
75+
76+
request->send(200, "text/plain", "Upload complete");
77+
},
78+
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
79+
Serial.printf("Upload[%s]: index=%u, len=%u, final=%d\n", filename.c_str(), index, len, final);
80+
81+
if (request->getResponse() != nullptr) {
82+
// upload aborted
83+
return;
84+
}
85+
86+
// start a new content-disposition upload
87+
if (!index) {
88+
// list all parameters
89+
const size_t params = request->params();
90+
for (size_t i = 0; i < params; i++) {
91+
const AsyncWebParameter *p = request->getParam(i);
92+
Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size());
93+
}
94+
95+
// get the content-disposition parameter
96+
const AsyncWebParameter *p = request->getParam(asyncsrv::T_name, true, true);
97+
if (p == nullptr) {
98+
request->send(400, "text/plain", "Missing content-disposition 'name' parameter");
99+
return;
100+
}
101+
102+
// determine upload type based on the parameter name
103+
if (p->value() == "fs") {
104+
Serial.printf("Filesystem image upload for file: %s\n", filename.c_str());
105+
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) {
106+
Update.printError(Serial);
107+
request->send(400, "text/plain", "Update begin failed");
108+
return;
109+
}
110+
111+
} else if (p->value() == "fw") {
112+
Serial.printf("Firmware image upload for file: %s\n", filename.c_str());
113+
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) {
114+
Update.printError(Serial);
115+
request->send(400, "text/plain", "Update begin failed");
116+
return;
117+
}
118+
119+
} else {
120+
Serial.printf("Unknown upload type for file: %s\n", filename.c_str());
121+
request->send(400, "text/plain", "Unknown upload type");
122+
return;
123+
}
124+
}
125+
126+
// some bytes to write ?
127+
if (len) {
128+
if (Update.write(data, len) != len) {
129+
Update.printError(Serial);
130+
Update.end();
131+
request->send(400, "text/plain", "Update write failed");
132+
return;
133+
}
134+
}
135+
136+
// finish the content-disposition upload
137+
if (final) {
138+
if (!Update.end(true)) {
139+
Update.printError(Serial);
140+
request->send(400, "text/plain", "Update end failed");
141+
return;
142+
}
143+
144+
// success response is created in the final request handler when all uploads are completed
145+
Serial.printf("Upload success of file %s\n", filename.c_str());
146+
}
147+
}
148+
);
149+
150+
#endif
151+
152+
server.begin();
153+
}
154+
155+
// not needed
156+
void loop() {
157+
delay(100);
158+
}

platformio.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ src_dir = examples/PerfTests
3434
; src_dir = examples/StaticFile
3535
; src_dir = examples/Templates
3636
; src_dir = examples/Upload
37+
; src_dir = examples/UploadFlash
3738
; src_dir = examples/URIMatcher
3839
; src_dir = examples/URIMatcherTest
3940
; src_dir = examples/WebSocket

src/WebRequest.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,16 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
520520
_itemFilename = nameVal;
521521
_itemIsFile = true;
522522
}
523+
// Add the parameters from the content-disposition header to the param list, flagged as POST and File,
524+
// so that they can be retrieved using getParam(name, isPost=true, isFile=true)
525+
// in the upload handler to correctly handle multiple file uploads within the same request.
526+
// Example: Content-Disposition: form-data; name="fw"; filename="firmware.bin"
527+
// See: https://github.com/ESP32Async/ESPAsyncWebServer/discussions/328
528+
if (_itemIsFile && _itemName.length() && _itemFilename.length()) {
529+
// add new parameters for this content-disposition
530+
_params.emplace_back(T_name, _itemName, true, true);
531+
_params.emplace_back(T_filename, _itemFilename, true, true);
532+
}
523533
}
524534
_temp = emptyString;
525535
} else {
@@ -593,6 +603,10 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
593603
}
594604
_itemBufferIndex = 0;
595605
_params.emplace_back(_itemName, _itemFilename, true, true, _itemSize);
606+
// remove previous occurrence(s) of content-disposition parameters for this upload
607+
_params.remove_if([this](const AsyncWebParameter &p) {
608+
return p.isPost() && p.isFile() && (p.name() == T_name || p.name() == T_filename);
609+
});
596610
free(_itemBuffer);
597611
_itemBuffer = NULL;
598612
}

0 commit comments

Comments
 (0)