Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/Upload/Upload.ino
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ void setup() {
if (!buffer->reserve(size)) {
delete buffer;
request->abort();
return;
}
request->_tempObject = buffer;
}
Expand Down Expand Up @@ -100,6 +101,7 @@ void setup() {

if (!request->_tempFile) {
request->send(400, "text/plain", "File not available for writing");
return;
}
}
if (len) {
Expand Down Expand Up @@ -141,6 +143,7 @@ void setup() {

// first pass ?
if (!index) {
// Note: using content type to determine size is not reliable!
size_t size = request->header("Content-Length").toInt();
if (!size) {
request->send(400, "text/plain", "No Content-Length");
Expand All @@ -150,6 +153,7 @@ void setup() {
if (!buffer) {
// not enough memory
request->abort();
return;
} else {
request->_tempObject = buffer;
}
Expand Down
158 changes: 158 additions & 0 deletions examples/UploadFlash/UploadFlash.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov

//
// Demo to upload a firmware and filesystem image via multipart form data
//

#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 <StreamString.h>
#include <LittleFS.h>

// ESP32 example ONLY
#ifdef ESP32
#include <Update.h>
#endif

static AsyncWebServer server(80);

void setup() {
Serial.begin(115200);

if (!LittleFS.begin()) {
LittleFS.format();
LittleFS.begin();
}

#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI || CONFIG_ESP32_WIFI_ENABLED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif

// ESP32 example ONLY
#ifdef ESP32

// Shows how to get the fw and fs (names) and filenames from a multipart upload,
// and also how to handle multiple file uploads in a single request.
//
// This example also shows how to pass and handle different parameters having the same name in query string, post form and content-disposition.
//
// Execute in the terminal, in order:
//
// 1. Build firmware: pio run -e arduino-3
// 2. Build FS image: pio run -e arduino-3 -t buildfs
// 3. Flash both at the same time: curl -v -F "name=Bob" -F "fw=@.pio/build/arduino-3/firmware.bin" -F "fs=@.pio/build/arduino-3/littlefs.bin" http://192.168.4.1/flash?name=Bill
//
server.on(
"/flash", HTTP_POST,
[](AsyncWebServerRequest *request) {
if (request->getResponse()) {
// response already created
return;
}

// list all parameters
Serial.println("Request parameters:");
const size_t params = request->params();
for (size_t i = 0; i < params; i++) {
const AsyncWebParameter *p = request->getParam(i);
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());
}

Serial.println("Flash / Filesystem upload completed");

request->send(200, "text/plain", "Upload complete");
},
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
Serial.printf("Upload[%s]: index=%u, len=%u, final=%d\n", filename.c_str(), index, len, final);

if (request->getResponse() != nullptr) {
// upload aborted
return;
}

// start a new content-disposition upload
if (!index) {
// list all parameters
const size_t params = request->params();
for (size_t i = 0; i < params; i++) {
const AsyncWebParameter *p = request->getParam(i);
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());
}

// get the content-disposition parameter
const AsyncWebParameter *p = request->getParam(asyncsrv::T_name, true, true);
if (p == nullptr) {
request->send(400, "text/plain", "Missing content-disposition 'name' parameter");
return;
}

// determine upload type based on the parameter name
if (p->value() == "fs") {
Serial.printf("Filesystem image upload for file: %s\n", filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) {
Update.printError(Serial);
request->send(400, "text/plain", "Update begin failed");
return;
}

} else if (p->value() == "fw") {
Serial.printf("Firmware image upload for file: %s\n", filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) {
Update.printError(Serial);
request->send(400, "text/plain", "Update begin failed");
return;
}

} else {
Serial.printf("Unknown upload type for file: %s\n", filename.c_str());
request->send(400, "text/plain", "Unknown upload type");
return;
}
}

// some bytes to write ?
if (len) {
if (Update.write(data, len) != len) {
Update.printError(Serial);
Update.end();
request->send(400, "text/plain", "Update write failed");
return;
}
}

// finish the content-disposition upload
if (final) {
if (!Update.end(true)) {
Update.printError(Serial);
request->send(400, "text/plain", "Update end failed");
return;
}

// success response is created in the final request handler when all uploads are completed
Serial.printf("Upload success of file %s\n", filename.c_str());
}
}
);

#endif

server.begin();
}

// not needed
void loop() {
delay(100);
}
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ src_dir = examples/PerfTests
; src_dir = examples/StaticFile
; src_dir = examples/Templates
; src_dir = examples/Upload
; src_dir = examples/UploadFlash
; src_dir = examples/URIMatcher
; src_dir = examples/URIMatcherTest
; src_dir = examples/WebSocket
Expand Down
14 changes: 14 additions & 0 deletions src/WebRequest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,16 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
_itemFilename = nameVal;
_itemIsFile = true;
}
// Add the parameters from the content-disposition header to the param list, flagged as POST and File,
// so that they can be retrieved using getParam(name, isPost=true, isFile=true)
// in the upload handler to correctly handle multiple file uploads within the same request.
// Example: Content-Disposition: form-data; name="fw"; filename="firmware.bin"
// See: https://github.com/ESP32Async/ESPAsyncWebServer/discussions/328
if (_itemIsFile && _itemName.length() && _itemFilename.length()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for my ignorance - is there a reason to require both name and filename before presenting either? Would it be better to treat them independently?

Copy link
Member Author

@mathieucarbou mathieucarbou Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They always come together: when creating a file upload in a form we pass the name and the file through html tags or JavaScript.

see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Disposition#as_a_header_for_a_multipart_body

The first directive is always form-data, and the header must also include a name parameter to identify the relevant field.

If not in a form multipart, then only the filename is known and it is passed always on the callback as done already.

This pr exposes the content disposition parameters in the case of a form upload and this is especially required when the form upload contains several files inside (multiple uploads in a single request) because the name parameter is the only way to distinguish these file uploads.

// add new parameters for this content-disposition
_params.emplace_back(T_name, _itemName, true, true);
_params.emplace_back(T_filename, _itemFilename, true, true);
}
}
_temp = emptyString;
} else {
Expand Down Expand Up @@ -593,6 +603,10 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
}
_itemBufferIndex = 0;
_params.emplace_back(_itemName, _itemFilename, true, true, _itemSize);
// remove previous occurrence(s) of content-disposition parameters for this upload
_params.remove_if([this](const AsyncWebParameter &p) {
return p.isPost() && p.isFile() && (p.name() == T_name || p.name() == T_filename);
});
free(_itemBuffer);
_itemBuffer = NULL;
}
Expand Down