From d56554100bcc2c4f7b39c104886f95abe5c5682f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Jan 2026 12:50:06 -1000 Subject: [PATCH] document document document --- .../update/esp32_hosted_update.cpp | 2 + .../components/http_request/http_request.h | 47 +++++++++++++++---- .../http_request/http_request_arduino.cpp | 8 ++-- .../http_request/http_request_idf.cpp | 10 ++-- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index 362151b322..93db9b7f02 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -185,6 +185,8 @@ bool Esp32HostedUpdate::fetch_manifest_() { } // Read manifest JSON into string (manifest is small, ~1KB max) + // NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h + // Use http_read_loop_result() helper instead of checking return values directly std::string json_str; json_str.reserve(container->content_length); uint8_t buf[256]; diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 57de00345e..30e205bf10 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -79,7 +79,35 @@ inline bool is_redirect(int const status) { */ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; } +/* + * HTTP Container Read Semantics + * ============================= + * + * IMPORTANT: These semantics differ from standard BSD sockets! + * + * BSD socket read() returns: + * > 0: bytes read + * == 0: connection closed (EOF) + * < 0: error (check errno) + * + * HttpContainer::read() returns: + * > 0: bytes read successfully + * == 0: no data available yet (non-blocking, caller should RETRY) + * < 0: error or connection closed (caller should EXIT) + * HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely + * other negative values = platform-specific errors + * + * This non-blocking design allows consistent behavior across: + * - ESP-IDF (async mode with EAGAIN handling) + * - Arduino (available() + connected() checks) + * + * 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 + */ + /// Error code returned by HttpContainer::read() when connection closed prematurely +/// NOTE: Unlike BSD sockets where 0 means EOF, here 0 means "no data yet, retry" static constexpr int HTTP_ERROR_CONNECTION_CLOSED = -1; /// Status of a read operation @@ -135,25 +163,26 @@ class HttpContainer : public Parented { uint32_t duration_ms; /** - * @brief Read data from the HTTP response body. + * @brief Read data from the HTTP response body (non-blocking). * - * This is a non-blocking read operation. The semantics are consistent across - * all platforms (Arduino and ESP-IDF): + * WARNING: These semantics differ from BSD sockets! + * BSD sockets: 0 = EOF (connection closed) + * This method: 0 = no data yet (retry), negative = error/closed * * @param buf Buffer to read data into * @param max_len Maximum number of bytes to read * @return * - > 0: Number of bytes read successfully - * - 0: No data available yet, caller should retry (data may still be arriving) + * - 0: No data available yet (NOT EOF!), caller should retry * - HTTP_ERROR_CONNECTION_CLOSED (-1): Connection closed prematurely * - < -1: Other error (platform-specific error code) * - * The caller should use get_bytes_read() and content_length to track progress. - * When get_bytes_read() >= content_length, all expected data has been received. + * Use get_bytes_read() and content_length to track progress. + * When get_bytes_read() >= content_length, all data has been received. * - * For non-blocking read loops, use http_read_loop_result() helper which handles - * timeout tracking and converts return values to HttpReadLoopResult enum. - * For simple buffer reads, use http_read_fully() helper. + * IMPORTANT: Do not use raw return values directly. Use these helpers: + * - http_read_loop_result(): for loops with per-chunk processing + * - http_read_fully(): for simple "read N bytes" operations */ virtual int read(uint8_t *buf, size_t max_len) = 0; virtual void end() = 0; diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 7eada01257..8ec4d2bc4b 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -141,6 +141,8 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // Arduino HTTP read implementation // +// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation. +// // Arduino's WiFiClient is inherently non-blocking - available() returns 0 when // no data is ready. We use connected() to distinguish "no data yet" from // "connection closed". @@ -150,10 +152,10 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // available() == 0 && connected(): no data yet, still connected // available() == 0 && !connected(): connection closed // -// We normalize these to the HttpContainer::read() contract: +// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!): // > 0: bytes read -// 0: no data yet, retry -// < 0: error (connection closed prematurely, or stream vanished) +// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF! +// < 0: error/connection closed <-- connection closed returns -1, not 0 int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index dfbb33c8a1..e01b2ee35c 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -212,17 +212,19 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c // ESP-IDF HTTP read implementation // +// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation. +// // Uses non-blocking mode (config.is_async = true) for consistent behavior with Arduino. // esp_http_client_read() in async mode returns: // > 0: bytes read -// 0: connection closed (end of stream) +// 0: connection closed (end of stream) <-- BSD socket EOF semantics // -ESP_ERR_HTTP_EAGAIN (0x7007): no data available yet (would block) // other negative: error // -// We normalize these to the HttpContainer::read() contract: +// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!): // > 0: bytes read -// 0: no data yet, retry -// < 0: error (connection closed prematurely, or other error) +// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF! +// < 0: error/connection closed <-- connection closed returns -1, not 0 int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());