Gate OTA session init on index==0 && len>0 instead of request pointer

The previous fix tracked the AsyncWebServerRequest pointer to distinguish
web_server_idf's double index==0 callbacks (Start marker with data==nullptr,
then the first real data chunk) from a retry after an interrupted upload.
That is unreliable: AsyncWebServerRequest is a stack-allocated object in the
httpd task, so a new request that happens to reuse the same stack address
as an interrupted one compares equal and silently skips the abort. Closing
a browser tab mid-upload and starting a fresh upload from another window
could then concatenate partial data from the first upload with the new
image.

Gate the init block on 'index == 0 && len > 0' instead. This uniquely
identifies the first real byte of an upload on both IDF (start-marker has
len==0) and Arduino, no identity tracking needed.
This commit is contained in:
J. Nick Koston
2026-04-13 12:56:52 -10:00
parent 3042a910fe
commit ba2558edf6
@@ -64,10 +64,6 @@ class OTARequestHandler : public AsyncWebHandler {
void report_ota_progress_(AsyncWebServerRequest *request);
void schedule_ota_reboot_();
void ota_init_(const char *filename);
void ota_end_session_() {
this->ota_backend_.reset();
this->ota_request_ = nullptr;
}
uint32_t last_ota_progress_{0};
uint32_t ota_read_length_{0};
@@ -76,11 +72,6 @@ class OTARequestHandler : public AsyncWebHandler {
private:
ota::OTABackendPtr ota_backend_{nullptr};
// Tracks the request that owns the current ota_backend_ session so we can
// detect when a new (retry) request arrives while a previous session is
// still open -- without re-triggering on the multiple index==0 callbacks
// web_server_idf makes for a single upload (Start + first data chunk).
AsyncWebServerRequest *ota_request_{nullptr};
};
void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
@@ -123,18 +114,20 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
uint8_t *data, size_t len, bool final) {
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK;
if (index == 0 && this->ota_request_ != request) {
// A new request is starting an upload. If a previous upload was interrupted
// (e.g. TCP reset) the backend from that session may still be open; tear it
// down so flash state doesn't get concatenated with the new image (which
// can produce a technically-valid-sized but corrupted firmware that
// bricks the device once it reboots).
// First byte of a new upload: index==0 with actual data. (web_server_idf
// fires a separate start-marker call with data==nullptr/len==0 before the
// first real chunk; gate on len>0 so we only trigger once per upload.)
if (index == 0 && len > 0) {
// If a previous upload was interrupted (e.g. client closed the tab, TCP
// reset) the backend from that session may still be open. Tear it down
// so flash state doesn't get concatenated with the new image (which can
// produce a technically-valid-sized but corrupted firmware that bricks
// the device once it reboots).
if (this->ota_backend_) {
ESP_LOGW(TAG, "New OTA upload received while previous session was still open; aborting previous session");
this->ota_backend_->abort();
this->ota_end_session_();
this->ota_backend_.reset();
}
this->ota_request_ = request;
// Initialize OTA on first call
this->ota_init_(filename.c_str());
@@ -169,7 +162,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
error_code = this->ota_backend_->begin(0);
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGE(TAG, "OTA begin failed: %d", error_code);
this->ota_end_session_();
this->ota_backend_.reset();
#ifdef USE_OTA_STATE_LISTENER
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
@@ -187,7 +180,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGE(TAG, "OTA write failed: %d", error_code);
this->ota_backend_->abort();
this->ota_end_session_();
this->ota_backend_.reset();
#ifdef USE_OTA_STATE_LISTENER
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
@@ -219,7 +212,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
}
this->ota_end_session_();
this->ota_backend_.reset();
}
}