From 3744186c3de9e7cb2e712eeef90055361ba8fb31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jan 2026 05:02:24 -1000 Subject: [PATCH 1/4] [http_request] Fix empty body for chunked transfer encoding responses --- .../http_request/http_request_arduino.cpp | 17 ++++++++++---- .../http_request/http_request_idf.cpp | 22 ++++++++++++++++--- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 8ec4d2bc4b..793ba2a128 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -131,6 +131,9 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur } } + // HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length). + // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit). + // The read() method handles this by checking content_length > 0 before using it. int content_length = container->client_.getSize(); ESP_LOGD(TAG, "Content-Length: %d", content_length); container->content_length = (size_t) content_length; @@ -167,17 +170,23 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { } int available_data = stream_ptr->available(); - int bufsize = std::min(max_len, std::min(this->content_length - this->bytes_read_, (size_t) available_data)); + // For chunked transfer encoding, content_length is 0 (unknown), so don't limit by it. + // HTTPClient::getSize() returns -1 for chunked, which becomes 0 when cast to size_t. + size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len; + int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data)); if (bufsize == 0) { this->duration_ms += (millis() - start); - // Check if we've read all expected content - if (this->bytes_read_ >= this->content_length) { + // Check if we've read all expected content (only valid when content_length is known) + // For chunked encoding (content_length == 0), we can't use this check + if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { return 0; // All content read successfully } // No data available - check if connection is still open + // For chunked encoding, !connected() after reading means EOF (all chunks received) + // For known content_length with bytes_read_ < content_length, it means connection dropped if (!stream_ptr->connected()) { - return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely + return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked } return 0; // No data yet, caller should retry } diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 680ae6c801..9ef5216a25 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -157,6 +157,9 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c } container->feed_wdt(); + // esp_http_client_fetch_headers() returns -1 for chunked transfer encoding (no Content-Length). + // When stored in size_t content_length, -1 becomes 0 due to the int64_t to size_t conversion. + // The read() method handles content_length == 0 specially to support chunked responses. container->content_length = esp_http_client_fetch_headers(client); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); @@ -225,14 +228,22 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c // // We normalize to HttpContainer::read() contract: // > 0: bytes read -// 0: no data yet / all content read (caller should check bytes_read vs content_length) +// 0: all content read (only returned when content_length is known and fully read) // < 0: error/connection closed +// +// Note on chunked transfer encoding: +// esp_http_client_fetch_headers() returns -1 for chunked responses, which becomes 0 +// when stored in size_t content_length. We handle this by skipping the content_length +// check when content_length is 0, allowing esp_http_client_read() to handle chunked +// decoding internally and signal EOF by returning 0. int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); // Check if we've already read all expected content - if (this->bytes_read_ >= this->content_length) { + // Skip this check when content_length is 0 (chunked transfer encoding or unknown length) + // For chunked responses, esp_http_client_read() will return 0 when all data is received + if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { return 0; // All content read successfully } @@ -247,7 +258,12 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { return read_len_or_error; } - // Connection closed by server before all content received + // esp_http_client_read() returns 0 in two cases: + // 1. Known content_length: connection closed before all data received (error) + // 2. Chunked encoding (content_length == 0): all data received (EOF) + // For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct. + // For case 2, it's semantically an EOF not an error, but functionally correct + // because all data is already in the caller's buffer at this point. if (read_len_or_error == 0) { return HTTP_ERROR_CONNECTION_CLOSED; } From e8ea90cb135fbd4194e7135785e3f640c87a50e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jan 2026 06:31:52 -1000 Subject: [PATCH 2/4] fix comment --- esphome/components/http_request/http_request_arduino.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 793ba2a128..046e5decff 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -170,15 +170,15 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { } int available_data = stream_ptr->available(); - // For chunked transfer encoding, content_length is 0 (unknown), so don't limit by it. - // HTTPClient::getSize() returns -1 for chunked, which becomes 0 when cast to size_t. + // For chunked transfer encoding, content_length is effectively unknown, so don't limit by it. + // HTTPClient::getSize() returns -1 for chunked, which becomes SIZE_MAX when cast to size_t. size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len; int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data)); if (bufsize == 0) { this->duration_ms += (millis() - start); - // Check if we've read all expected content (only valid when content_length is known) - // For chunked encoding (content_length == 0), we can't use this check + // Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX) + // For chunked encoding (content_length == SIZE_MAX), we can't use this check if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { return 0; // All content read successfully } From 0c868cbcc5e579459bc99c60bbc4f55efdc80521 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jan 2026 06:38:01 -1000 Subject: [PATCH 3/4] adjust comments, cases were reversed as I had the wrong file open --- .../http_request/http_request_arduino.cpp | 3 ++- .../http_request/http_request_idf.cpp | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 046e5decff..16f999c671 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -133,7 +133,8 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length). // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit). - // The read() method handles this by checking content_length > 0 before using it. + // The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the + // early return check (bytes_read_ >= content_length) will never trigger. int content_length = container->client_.getSize(); ESP_LOGD(TAG, "Content-Length: %d", content_length); container->content_length = (size_t) content_length; diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 9ef5216a25..2b4dee953a 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -157,8 +157,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c } container->feed_wdt(); - // esp_http_client_fetch_headers() returns -1 for chunked transfer encoding (no Content-Length). - // When stored in size_t content_length, -1 becomes 0 due to the int64_t to size_t conversion. + // esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header). // The read() method handles content_length == 0 specially to support chunked responses. container->content_length = esp_http_client_fetch_headers(client); container->feed_wdt(); @@ -232,10 +231,10 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c // < 0: error/connection closed // // Note on chunked transfer encoding: -// esp_http_client_fetch_headers() returns -1 for chunked responses, which becomes 0 -// when stored in size_t content_length. We handle this by skipping the content_length -// check when content_length is 0, allowing esp_http_client_read() to handle chunked -// decoding internally and signal EOF by returning 0. +// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header). +// We handle this by skipping the content_length check when content_length is 0, +// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF +// by returning 0. int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); @@ -260,10 +259,11 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { // esp_http_client_read() returns 0 in two cases: // 1. Known content_length: connection closed before all data received (error) - // 2. Chunked encoding (content_length == 0): all data received (EOF) + // 2. Chunked encoding (content_length == 0): end of stream reached (EOF) // For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct. - // For case 2, it's semantically an EOF not an error, but functionally correct - // because all data is already in the caller's buffer at this point. + // For case 2, 0 indicates that all chunked data has already been delivered + // in previous successful read() calls, so treating this as a closed + // connection does not cause any loss of response data. if (read_len_or_error == 0) { return HTTP_ERROR_CONNECTION_CLOSED; } From 4e67898073c08b5f4b76d541b3e81dc53013be86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jan 2026 06:45:14 -1000 Subject: [PATCH 4/4] improve comment --- esphome/components/http_request/http_request_arduino.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 16f999c671..82538b2cb3 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -171,8 +171,8 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { } int available_data = stream_ptr->available(); - // For chunked transfer encoding, content_length is effectively unknown, so don't limit by it. - // HTTPClient::getSize() returns -1 for chunked, which becomes SIZE_MAX when cast to size_t. + // For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when + // cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read. size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len; int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));