From 6596f864be04ce27831a2fdd6e45af96c7c4e2af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 14:35:38 -0500 Subject: [PATCH] merg3 --- esphome/components/web_server_idf/__init__.py | 10 +- .../web_server_idf/multipart_parser.cpp | 253 ------------------ .../web_server_idf/multipart_parser.h | 75 ------ .../web_server_idf/multipart_reader.cpp | 193 +++++++++++++ .../web_server_idf/multipart_reader.h | 65 +++++ .../web_server_idf/web_server_idf.cpp | 87 ++++-- 6 files changed, 327 insertions(+), 356 deletions(-) delete mode 100644 esphome/components/web_server_idf/multipart_parser.cpp delete mode 100644 esphome/components/web_server_idf/multipart_parser.h create mode 100644 esphome/components/web_server_idf/multipart_reader.cpp create mode 100644 esphome/components/web_server_idf/multipart_reader.h diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py index 506e1c5c13..03f8e60715 100644 --- a/esphome/components/web_server_idf/__init__.py +++ b/esphome/components/web_server_idf/__init__.py @@ -1,5 +1,7 @@ -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option import esphome.config_validation as cv +from esphome.const import CONF_OTA +from esphome.core import CORE CODEOWNERS = ["@dentra"] @@ -12,3 +14,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024) + + # Check if web_server component has OTA enabled + web_server_config = CORE.config.get("web_server", {}) + if web_server_config.get(CONF_OTA, True): # OTA is enabled by default + # Add multipart parser component for OTA support + add_idf_component(name="zorxx/multipart-parser", ref="1.0.1") diff --git a/esphome/components/web_server_idf/multipart_parser.cpp b/esphome/components/web_server_idf/multipart_parser.cpp deleted file mode 100644 index 6576951a4f..0000000000 --- a/esphome/components/web_server_idf/multipart_parser.cpp +++ /dev/null @@ -1,253 +0,0 @@ -#ifdef USE_ESP_IDF -#ifdef USE_WEBSERVER_OTA -#include "multipart_parser.h" -#include "multipart_parser_utils.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace web_server_idf { - -static const char *const TAG = "multipart_parser"; - -// Constants for multipart parsing -static constexpr size_t CRLF_LENGTH = 2; -static constexpr size_t MIN_BOUNDARY_BUFFER = 4; // Extra bytes to keep for split boundary detection -static constexpr const char *CRLF_STR = "\r\n"; - -bool MultipartParser::parse(const uint8_t *data, size_t len) { - // Append new data to buffer - if (data && len > 0) { - buffer_.insert(buffer_.end(), data, data + len); - } - - // Limit iterations to prevent infinite loops - static constexpr size_t MAX_ITERATIONS = 10; - size_t iterations = 0; - - bool made_progress = true; - while (made_progress && state_ != DONE && state_ != ERROR && !buffer_.empty() && iterations < MAX_ITERATIONS) { - made_progress = false; - iterations++; - - switch (state_) { - case BOUNDARY_SEARCH: - if (find_boundary()) { - state_ = HEADERS; - made_progress = true; - } - break; - - case HEADERS: - if (parse_headers()) { - state_ = CONTENT; - made_progress = true; - } - break; - - case CONTENT: - if (extract_content()) { - // Content is ready, return to caller - return true; - } - // If we're waiting for more data in CONTENT state, exit the loop - return false; - - default: - ESP_LOGE(TAG, "Invalid parser state: %d", state_); - state_ = ERROR; - break; - } - } - - if (iterations >= MAX_ITERATIONS) { - ESP_LOGW(TAG, "Parser reached maximum iterations, possible malformed data"); - } - - return part_ready_; -} - -bool MultipartParser::get_current_part(Part &part) const { - if (!part_ready_ || content_length_ == 0) { - return false; - } - - part.name = current_name_; - part.filename = current_filename_; - part.content_type = current_content_type_; - part.data = buffer_.data(); - part.length = content_length_; - - return true; -} - -void MultipartParser::consume_part() { - if (!part_ready_) { - return; - } - - // Remove consumed data from buffer - if (content_length_ < buffer_.size()) { - buffer_.erase(buffer_.begin(), buffer_.begin() + content_length_); - } else { - buffer_.clear(); - } - - // Reset for next part - part_ready_ = false; - content_length_ = 0; - current_name_.clear(); - current_filename_.clear(); - current_content_type_.clear(); - - // Look for next boundary - state_ = BOUNDARY_SEARCH; -} - -void MultipartParser::reset() { - buffer_.clear(); - state_ = BOUNDARY_SEARCH; - part_ready_ = false; - content_length_ = 0; - current_name_.clear(); - current_filename_.clear(); - current_content_type_.clear(); -} - -bool MultipartParser::find_boundary() { - // Look for boundary in buffer - size_t boundary_pos = find_pattern(reinterpret_cast(boundary_.c_str()), boundary_.length()); - - if (boundary_pos == std::string::npos) { - // Keep some data for next iteration to handle split boundaries - if (buffer_.size() > boundary_.length() + MIN_BOUNDARY_BUFFER) { - buffer_.erase(buffer_.begin(), buffer_.end() - boundary_.length() - MIN_BOUNDARY_BUFFER); - } - return false; - } - - // Remove everything up to and including the boundary - buffer_.erase(buffer_.begin(), buffer_.begin() + boundary_pos + boundary_.length()); - - // Skip CRLF after boundary - if (buffer_.size() >= CRLF_LENGTH && buffer_[0] == '\r' && buffer_[1] == '\n') { - buffer_.erase(buffer_.begin(), buffer_.begin() + CRLF_LENGTH); - } - - // Check if this is the end boundary - if (buffer_.size() >= CRLF_LENGTH && buffer_[0] == '-' && buffer_[1] == '-') { - state_ = DONE; - return false; - } - - return true; -} - -bool MultipartParser::parse_headers() { - // Limit header lines to prevent DOS attacks - static constexpr size_t MAX_HEADER_LINES = 50; - size_t header_count = 0; - - while (header_count < MAX_HEADER_LINES) { - std::string line = read_line(); - if (line.empty()) { - // Check if we have enough data for a line - auto crlf_pos = find_pattern(reinterpret_cast(CRLF_STR), CRLF_LENGTH); - if (crlf_pos == std::string::npos) { - return false; // Need more data - } - // Empty line means headers are done - buffer_.erase(buffer_.begin(), buffer_.begin() + CRLF_LENGTH); - return true; - } - - process_header_line(line); - header_count++; - } - - ESP_LOGW(TAG, "Too many headers in multipart data"); - state_ = ERROR; - return false; -} - -void MultipartParser::process_header_line(const std::string &line) { - if (str_startswith_case_insensitive(line, "content-disposition:")) { - // Extract name and filename parameters - current_name_ = extract_header_param(line, "name"); - current_filename_ = extract_header_param(line, "filename"); - } else if (str_startswith_case_insensitive(line, "content-type:")) { - current_content_type_ = extract_header_value(line); - } - // RFC 7578: Ignore any other Content-* headers -} - -bool MultipartParser::extract_content() { - // Look for next boundary - std::string search_boundary = CRLF_STR + boundary_; - size_t boundary_pos = - find_pattern(reinterpret_cast(search_boundary.c_str()), search_boundary.length()); - - if (boundary_pos != std::string::npos) { - // Found complete part - content_length_ = boundary_pos; - part_ready_ = true; - return true; - } - - // No boundary found yet, but we might have partial content - // Keep enough bytes to ensure we don't split a boundary - size_t safe_length = buffer_.size(); - if (safe_length > search_boundary.length() + MIN_BOUNDARY_BUFFER) { - safe_length -= search_boundary.length() + MIN_BOUNDARY_BUFFER; - if (safe_length > 0) { - content_length_ = safe_length; - // We have partial content but not complete yet - return false; - } - } - - return false; -} - -std::string MultipartParser::read_line() { - // Limit line length to prevent excessive memory usage - static constexpr size_t MAX_LINE_LENGTH = 4096; - - auto crlf_pos = find_pattern(reinterpret_cast(CRLF_STR), CRLF_LENGTH); - if (crlf_pos == std::string::npos) { - // If we have too much data without CRLF, it's likely malformed - if (buffer_.size() > MAX_LINE_LENGTH) { - ESP_LOGW(TAG, "Header line too long, truncating"); - state_ = ERROR; - } - return ""; - } - - if (crlf_pos > MAX_LINE_LENGTH) { - ESP_LOGW(TAG, "Header line exceeds maximum length"); - state_ = ERROR; - return ""; - } - - std::string line(buffer_.begin(), buffer_.begin() + crlf_pos); - buffer_.erase(buffer_.begin(), buffer_.begin() + crlf_pos + CRLF_LENGTH); - return line; -} - -size_t MultipartParser::find_pattern(const uint8_t *pattern, size_t pattern_len, size_t start) const { - if (buffer_.size() < pattern_len + start) { - return std::string::npos; - } - - for (size_t i = start; i <= buffer_.size() - pattern_len; ++i) { - if (memcmp(buffer_.data() + i, pattern, pattern_len) == 0) { - return i; - } - } - - return std::string::npos; -} - -} // namespace web_server_idf -} // namespace esphome -#endif // USE_WEBSERVER_OTA -#endif // USE_ESP_IDF diff --git a/esphome/components/web_server_idf/multipart_parser.h b/esphome/components/web_server_idf/multipart_parser.h deleted file mode 100644 index 480b35a5a1..0000000000 --- a/esphome/components/web_server_idf/multipart_parser.h +++ /dev/null @@ -1,75 +0,0 @@ -#pragma once -#ifdef USE_ESP_IDF -#ifdef USE_WEBSERVER_OTA - -#include -#include -#include - -namespace esphome { -namespace web_server_idf { - -// Multipart form data parser for ESP-IDF -// Implements RFC 7578 compliant multipart/form-data parsing -class MultipartParser { - public: - static constexpr const char *MULTIPART_BOUNDARY_PREFIX = "--"; - - enum State : uint8_t { BOUNDARY_SEARCH, HEADERS, CONTENT, DONE, ERROR }; - - struct Part { - std::string name; - std::string filename; - std::string content_type; - const uint8_t *data; - size_t length; - }; - - explicit MultipartParser(const std::string &boundary) - : boundary_(MULTIPART_BOUNDARY_PREFIX + boundary), - state_(BOUNDARY_SEARCH), - content_length_(0), - part_ready_(false) {} - - // Process incoming data chunk - // Returns true if a complete part is available - bool parse(const uint8_t *data, size_t len); - - // Get the current part if available - bool get_current_part(Part &part) const; - - // Consume the current part and move to next - void consume_part(); - - State get_state() const { return state_; } - bool is_done() const { return state_ == DONE; } - bool has_error() const { return state_ == ERROR; } - - // Reset parser for reuse - void reset(); - - private: - bool find_boundary(); - bool parse_headers(); - void process_header_line(const std::string &line); - bool extract_content(); - - std::string read_line(); - size_t find_pattern(const uint8_t *pattern, size_t pattern_len, size_t start = 0) const; - - std::string boundary_; - State state_; - std::vector buffer_; - - // Current part info - std::string current_name_; - std::string current_filename_; - std::string current_content_type_; - size_t content_length_{0}; - bool part_ready_{false}; -}; - -} // namespace web_server_idf -} // namespace esphome -#endif // USE_WEBSERVER_OTA -#endif // USE_ESP_IDF diff --git a/esphome/components/web_server_idf/multipart_reader.cpp b/esphome/components/web_server_idf/multipart_reader.cpp new file mode 100644 index 0000000000..f157fe91e1 --- /dev/null +++ b/esphome/components/web_server_idf/multipart_reader.cpp @@ -0,0 +1,193 @@ +#ifdef USE_ESP_IDF +#ifdef USE_WEBSERVER_OTA +#include "multipart_reader.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome { +namespace web_server_idf { + +static const char *const TAG = "multipart_reader"; + +MultipartReader::MultipartReader(const std::string &boundary) { + // Initialize settings with callbacks + memset(&settings_, 0, sizeof(settings_)); + settings_.on_header_field = on_header_field; + settings_.on_header_value = on_header_value; + settings_.on_part_data_begin = on_part_data_begin; + settings_.on_part_data = on_part_data; + settings_.on_part_data_end = on_part_data_end; + settings_.on_headers_complete = on_headers_complete; + + // Create parser with boundary + parser_ = multipart_parser_init(boundary.c_str(), &settings_); + if (parser_) { + multipart_parser_set_data(parser_, this); + } +} + +MultipartReader::~MultipartReader() { + if (parser_) { + multipart_parser_free(parser_); + } +} + +size_t MultipartReader::parse(const char *data, size_t len) { + if (!parser_) { + return 0; + } + return multipart_parser_execute(parser_, data, len); +} + +int MultipartReader::on_header_field(multipart_parser *parser, const char *at, size_t length) { + MultipartReader *reader = static_cast(multipart_parser_get_data(parser)); + + // If we were processing a value, save it + if (!reader->current_header_value_.empty()) { + // Process the previous header + std::string field_lower = reader->current_header_field_; + std::transform(field_lower.begin(), field_lower.end(), field_lower.begin(), ::tolower); + + if (field_lower == "content-disposition") { + // Parse name and filename from Content-Disposition + size_t name_pos = reader->current_header_value_.find("name="); + if (name_pos != std::string::npos) { + name_pos += 5; + size_t end_pos; + if (reader->current_header_value_[name_pos] == '"') { + name_pos++; + end_pos = reader->current_header_value_.find('"', name_pos); + } else { + end_pos = reader->current_header_value_.find_first_of("; \r\n", name_pos); + } + if (end_pos != std::string::npos) { + reader->current_part_.name = reader->current_header_value_.substr(name_pos, end_pos - name_pos); + } + } + + size_t filename_pos = reader->current_header_value_.find("filename="); + if (filename_pos != std::string::npos) { + filename_pos += 9; + size_t end_pos; + if (reader->current_header_value_[filename_pos] == '"') { + filename_pos++; + end_pos = reader->current_header_value_.find('"', filename_pos); + } else { + end_pos = reader->current_header_value_.find_first_of("; \r\n", filename_pos); + } + if (end_pos != std::string::npos) { + reader->current_part_.filename = reader->current_header_value_.substr(filename_pos, end_pos - filename_pos); + } + } + } else if (field_lower == "content-type") { + reader->current_part_.content_type = reader->current_header_value_; + } + + reader->current_header_value_.clear(); + } + + // Start new header field + reader->current_header_field_.assign(at, length); + reader->in_headers_ = true; + + return 0; +} + +int MultipartReader::on_header_value(multipart_parser *parser, const char *at, size_t length) { + MultipartReader *reader = static_cast(multipart_parser_get_data(parser)); + reader->current_header_value_.append(at, length); + return 0; +} + +int MultipartReader::on_headers_complete(multipart_parser *parser) { + MultipartReader *reader = static_cast(multipart_parser_get_data(parser)); + + // Process last header if any + if (!reader->current_header_value_.empty()) { + std::string field_lower = reader->current_header_field_; + std::transform(field_lower.begin(), field_lower.end(), field_lower.begin(), ::tolower); + + if (field_lower == "content-disposition") { + // Parse name and filename from Content-Disposition + size_t name_pos = reader->current_header_value_.find("name="); + if (name_pos != std::string::npos) { + name_pos += 5; + size_t end_pos; + if (reader->current_header_value_[name_pos] == '"') { + name_pos++; + end_pos = reader->current_header_value_.find('"', name_pos); + } else { + end_pos = reader->current_header_value_.find_first_of("; \r\n", name_pos); + } + if (end_pos != std::string::npos) { + reader->current_part_.name = reader->current_header_value_.substr(name_pos, end_pos - name_pos); + } + } + + size_t filename_pos = reader->current_header_value_.find("filename="); + if (filename_pos != std::string::npos) { + filename_pos += 9; + size_t end_pos; + if (reader->current_header_value_[filename_pos] == '"') { + filename_pos++; + end_pos = reader->current_header_value_.find('"', filename_pos); + } else { + end_pos = reader->current_header_value_.find_first_of("; \r\n", filename_pos); + } + if (end_pos != std::string::npos) { + reader->current_part_.filename = reader->current_header_value_.substr(filename_pos, end_pos - filename_pos); + } + } + } else if (field_lower == "content-type") { + reader->current_part_.content_type = reader->current_header_value_; + } + } + + reader->in_headers_ = false; + reader->current_header_field_.clear(); + reader->current_header_value_.clear(); + + ESP_LOGD(TAG, "Part headers complete: name='%s', filename='%s', content_type='%s'", + reader->current_part_.name.c_str(), reader->current_part_.filename.c_str(), + reader->current_part_.content_type.c_str()); + + return 0; +} + +int MultipartReader::on_part_data_begin(multipart_parser *parser) { + MultipartReader *reader = static_cast(multipart_parser_get_data(parser)); + ESP_LOGD(TAG, "Part data begin"); + return 0; +} + +int MultipartReader::on_part_data(multipart_parser *parser, const char *at, size_t length) { + MultipartReader *reader = static_cast(multipart_parser_get_data(parser)); + + // Only process file uploads + if (reader->has_file() && reader->data_callback_) { + reader->data_callback_(reinterpret_cast(at), length); + } + + return 0; +} + +int MultipartReader::on_part_data_end(multipart_parser *parser) { + MultipartReader *reader = static_cast(multipart_parser_get_data(parser)); + + ESP_LOGD(TAG, "Part data end"); + + if (reader->part_complete_callback_) { + reader->part_complete_callback_(); + } + + // Clear part info for next part + reader->current_part_ = Part{}; + + return 0; +} + +} // namespace web_server_idf +} // namespace esphome +#endif // USE_WEBSERVER_OTA +#endif // USE_ESP_IDF \ No newline at end of file diff --git a/esphome/components/web_server_idf/multipart_reader.h b/esphome/components/web_server_idf/multipart_reader.h new file mode 100644 index 0000000000..e54939e045 --- /dev/null +++ b/esphome/components/web_server_idf/multipart_reader.h @@ -0,0 +1,65 @@ +#pragma once +#ifdef USE_ESP_IDF +#ifdef USE_WEBSERVER_OTA + +#include +#include +#include +#include + +namespace esphome { +namespace web_server_idf { + +// Wrapper around zorxx/multipart-parser for ESP-IDF OTA uploads +class MultipartReader { + public: + struct Part { + std::string name; + std::string filename; + std::string content_type; + }; + + using DataCallback = std::function; + using PartCompleteCallback = std::function; + + explicit MultipartReader(const std::string &boundary); + ~MultipartReader(); + + // Set callbacks for handling data + void set_data_callback(DataCallback callback) { data_callback_ = callback; } + void set_part_complete_callback(PartCompleteCallback callback) { part_complete_callback_ = callback; } + + // Parse incoming data + size_t parse(const char *data, size_t len); + + // Get current part info + const Part &get_current_part() const { return current_part_; } + + // Check if we found a file upload + bool has_file() const { return !current_part_.filename.empty(); } + + private: + static int on_header_field(multipart_parser *parser, const char *at, size_t length); + static int on_header_value(multipart_parser *parser, const char *at, size_t length); + static int on_part_data_begin(multipart_parser *parser); + static int on_part_data(multipart_parser *parser, const char *at, size_t length); + static int on_part_data_end(multipart_parser *parser); + static int on_headers_complete(multipart_parser *parser); + + multipart_parser *parser_{nullptr}; + multipart_parser_settings settings_{}; + + Part current_part_; + std::string current_header_field_; + std::string current_header_value_; + + DataCallback data_callback_; + PartCompleteCallback part_complete_callback_; + + bool in_headers_{false}; +}; + +} // namespace web_server_idf +} // namespace esphome +#endif // USE_WEBSERVER_OTA +#endif // USE_ESP_IDF \ No newline at end of file diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 93425862d2..775d5727d3 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -9,8 +9,7 @@ #include "utils.h" #ifdef USE_WEBSERVER_OTA -#include "multipart_parser.h" -#include "multipart_parser_utils.h" +#include "multipart_reader.h" #endif #include "web_server_idf.h" @@ -79,16 +78,30 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { #ifdef USE_WEBSERVER_OTA // Check if this is a multipart form data request (for OTA updates) - const char *boundary_start = nullptr; - size_t boundary_len = 0; bool is_multipart = false; + std::string boundary; if (content_type.has_value()) { - const char *ct = content_type.value().c_str(); - is_multipart = parse_multipart_boundary(ct, &boundary_start, &boundary_len); + const std::string &ct = content_type.value(); + size_t boundary_pos = ct.find("boundary="); + if (boundary_pos != std::string::npos) { + boundary_pos += 9; // Skip "boundary=" + size_t boundary_end = ct.find_first_of(" ;\r\n", boundary_pos); + if (boundary_end == std::string::npos) { + boundary_end = ct.length(); + } + if (ct[boundary_pos] == '"' && boundary_end > boundary_pos + 1 && ct[boundary_end - 1] == '"') { + // Quoted boundary + boundary = ct.substr(boundary_pos + 1, boundary_end - boundary_pos - 2); + } else { + // Unquoted boundary + boundary = ct.substr(boundary_pos, boundary_end - boundary_pos); + } + is_multipart = ct.find("multipart/form-data") != std::string::npos && !boundary.empty(); + } - if (!is_multipart && !is_form_urlencoded(ct)) { - ESP_LOGW(TAG, "Unsupported content type for POST: %s", ct); + if (!is_multipart && ct.find("application/x-www-form-urlencoded") == std::string::npos) { + ESP_LOGW(TAG, "Unsupported content type for POST: %s", ct.c_str()); // fallback to get handler to support backward compatibility return AsyncWebServer::request_handler(r); } @@ -109,7 +122,7 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { #ifdef USE_WEBSERVER_OTA // Handle multipart form data - if (is_multipart && boundary_start && boundary_len > 0) { + if (is_multipart && !boundary.empty()) { // Create request object AsyncWebServerRequest req(r); auto *server = static_cast(r->user_ctx); @@ -128,18 +141,36 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { return ESP_OK; } - // Handle multipart upload - create boundary string only when needed - std::string boundary(boundary_start, boundary_len); - MultipartParser parser(boundary); + // Handle multipart upload using the multipart-parser library + MultipartReader reader(boundary); static constexpr size_t CHUNK_SIZE = 1024; - uint8_t *chunk_buf = new uint8_t[CHUNK_SIZE]; + char *chunk_buf = new char[CHUNK_SIZE]; size_t total_len = r->content_len; size_t remaining = total_len; - bool first_part = true; + std::string current_filename; + bool upload_started = false; + + // Set up callbacks for the multipart reader + reader.set_data_callback([&](const uint8_t *data, size_t len) { + if (!current_filename.empty()) { + found_handler->handleUpload(&req, current_filename, upload_started ? 1 : 0, const_cast(data), len, + false); + upload_started = true; + } + }); + + reader.set_part_complete_callback([&]() { + if (!current_filename.empty() && upload_started) { + // Signal end of this part + found_handler->handleUpload(&req, current_filename, 2, nullptr, 0, false); + current_filename.clear(); + upload_started = false; + } + }); while (remaining > 0) { size_t to_read = std::min(remaining, CHUNK_SIZE); - int recv_len = httpd_req_recv(r, reinterpret_cast(chunk_buf), to_read); + int recv_len = httpd_req_recv(r, chunk_buf, to_read); if (recv_len <= 0) { delete[] chunk_buf; @@ -152,23 +183,25 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { } // Parse multipart data - if (parser.parse(chunk_buf, recv_len)) { - MultipartParser::Part part; - if (parser.get_current_part(part) && !part.filename.empty()) { - // This is a file upload - found_handler->handleUpload(&req, part.filename, first_part ? 0 : 1, const_cast(part.data), - part.length, false); - first_part = false; - parser.consume_part(); - } + size_t parsed = reader.parse(chunk_buf, recv_len); + if (parsed != recv_len) { + ESP_LOGW(TAG, "Multipart parser error at byte %zu", total_len - remaining + parsed); + delete[] chunk_buf; + httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); + return ESP_FAIL; + } + + // Check if we found a new file part + if (reader.has_file() && current_filename.empty()) { + current_filename = reader.get_current_part().filename; } remaining -= recv_len; } - // Final call to handler - if (!first_part) { - found_handler->handleUpload(&req, "", 2, nullptr, 0, true); + // Final cleanup - send final signal if upload was in progress + if (!current_filename.empty() && upload_started) { + found_handler->handleUpload(&req, current_filename, 2, nullptr, 0, true); } delete[] chunk_buf;