diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index c88360ca78..a427cc4a05 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -103,6 +103,42 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st * - ESP-IDF: blocking reads, 0 only returned when all content read * - Arduino: non-blocking, 0 means "no data yet" or "all content read" * + * Chunked responses that complete in a reasonable time work correctly on both + * platforms. The limitation below applies only to *streaming* chunked + * responses where data arrives slowly over a long period. + * + * Streaming chunked responses are NOT supported (all platforms): + * The read helpers (http_read_loop_result, http_read_fully) block the main + * event loop until all response data is received. For streaming responses + * where data trickles in slowly (e.g., TTS streaming via ffmpeg proxy), + * this starves the event loop on both ESP-IDF and Arduino. If data arrives + * just often enough to avoid the caller's timeout, the loop runs + * indefinitely. If data stops entirely, ESP-IDF fails with + * -ESP_ERR_HTTP_EAGAIN (transport timeout) while Arduino spins with + * delay(1) until the caller's timeout fires. Supporting streaming requires + * a non-blocking incremental read pattern that yields back to the event + * loop between chunks. Components that need streaming should use + * esp_http_client directly on a separate FreeRTOS task with + * esp_http_client_is_complete_data_received() for completion detection + * (see audio_reader.cpp for an example). + * + * Chunked transfer encoding - platform differences: + * - ESP-IDF HttpContainer: + * HttpContainerIDF overrides is_read_complete() to call + * esp_http_client_is_complete_data_received(), which is the + * authoritative completion check for both chunked and non-chunked + * transfers. When esp_http_client_read() returns 0 for a completed + * chunked response, read() returns 0 and is_read_complete() returns + * true, so callers get COMPLETE from http_read_loop_result(). + * + * - Arduino HttpContainer: + * Chunked responses are decoded internally (see + * HttpContainerArduino::read_chunked_()). When the final chunk arrives, + * is_chunked_ is cleared and content_length is set to bytes_read_. + * Completion is then detected via is_read_complete(), and a subsequent + * read() returns 0 to indicate "all content read" (not + * HTTP_ERROR_CONNECTION_CLOSED). + * * Use the helper functions below instead of checking return values directly: * - http_read_loop_result(): for manual loops with per-chunk processing * - http_read_fully(): for simple "read N bytes into buffer" operations @@ -204,9 +240,13 @@ class HttpContainer : public Parented { size_t get_bytes_read() const { return this->bytes_read_; } - /// Check if all expected content has been read - /// For chunked responses, returns false (completion detected via read() returning error/EOF) - bool is_read_complete() const { + /// Check if all expected content has been read. + /// Base implementation handles non-chunked responses and status-code-based no-body checks. + /// Platform implementations may override for chunked completion detection: + /// - ESP-IDF: overrides to call esp_http_client_is_complete_data_received() for chunked. + /// - Arduino: read_chunked_() clears is_chunked_ and sets content_length on the final + /// chunk, after which the base implementation detects completion. + virtual bool is_read_complete() const { // Per RFC 9112, these responses have no body: // - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT || diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index bd12b7d123..486984a694 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -218,32 +218,50 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c return container; } +bool HttpContainerIDF::is_read_complete() const { + // Base class handles no-body status codes and non-chunked content_length completion + if (HttpContainer::is_read_complete()) { + return true; + } + // For chunked responses, use the authoritative ESP-IDF completion check + return this->is_chunked_ && esp_http_client_is_complete_data_received(this->client_); +} + // ESP-IDF HTTP read implementation (blocking mode) // // WARNING: Return values differ from BSD sockets! See http_request.h for full documentation. // // esp_http_client_read() in blocking mode returns: // > 0: bytes read -// 0: connection closed (end of stream) +// 0: all chunked data received (is_chunk_complete true) or connection closed +// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet // < 0: error // // We normalize to HttpContainer::read() contract: // > 0: bytes read -// 0: all content read (only returned when content_length is known and fully read) +// 0: all content read (for both content_length-based and chunked completion) // < 0: error/connection closed // // Note on chunked transfer encoding: // 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. +// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls +// esp_http_client_is_complete_data_received() to distinguish successful completion from +// connection errors. Callers use http_read_loop_result() which checks is_read_complete() +// to return COMPLETE for successful chunked EOF. +// +// Streaming chunked responses are not supported (see http_request.h for details). +// When data stops arriving, esp_http_client_read() returns -ESP_ERR_HTTP_EAGAIN +// after its internal transport timeout (configured via timeout_ms) expires. +// This is passed through as a negative return value, which callers treat as an error. 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 (non-chunked only) - // For chunked responses (content_length == 0), esp_http_client_read() handles EOF - if (this->is_read_complete()) { + // Check if we've already read all expected content (non-chunked and no-body only). + // Use the base class check here, NOT the override: esp_http_client_is_complete_data_received() + // returns true as soon as all data arrives from the network, but data may still be in + // the client's internal buffer waiting to be consumed by esp_http_client_read(). + if (HttpContainer::is_read_complete()) { return 0; // All content read successfully } @@ -258,15 +276,18 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { return read_len_or_error; } - // 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): end of stream reached (EOF) - // For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct. - // 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. + // esp_http_client_read() returns 0 when: + // - Known content_length: connection closed before all data received (error) + // - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF) + // + // Return 0 in both cases. Callers use http_read_loop_result() which calls + // is_read_complete() to distinguish these: + // - Chunked complete: is_read_complete() returns true (via + // esp_http_client_is_complete_data_received()), caller gets COMPLETE + // - Non-chunked incomplete: is_read_complete() returns false, caller + // eventually gets TIMEOUT (since no more data arrives) if (read_len_or_error == 0) { - return HTTP_ERROR_CONNECTION_CLOSED; + return 0; } // Negative value - error, return the actual error code for debugging diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index ad11811a8f..2a130eae58 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -16,6 +16,7 @@ class HttpContainerIDF : public HttpContainer { HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {} int read(uint8_t *buf, size_t max_len) override; void end() override; + bool is_read_complete() const override; /// @brief Feeds the watchdog timer if the executing task has one attached void feed_wdt();