fix(esp_http_client): fix parser wedge and dead error-classification branches on read timeout (IDFGH-17949)#18818
Open
94xhn wants to merge 1 commit into
Open
Conversation
Collaborator
|
Hey @94xhn, thanks for raising this PR. The changes look good, but can you please help to update the commit message The current commit message says This is incorrect as the client doesn't block forever here. The consequence here is silent truncation is treated as success. |
Ashish285
reviewed
Jul 13, 2026
A blocking transport read can return zero when no data arrives before timeout. Passing that zero length to http_parser_execute signals EOF while a response is incomplete, puts the parser in HPE_INVALID_EOF_STATE, and causes later response bytes to be discarded. The shortened response can then be treated as successful. Skip parser execution for every zero-length transport read, not only async reads. Also compare the raw esp_transport_read result against raw ERR_TCP_TRANSPORT values so timeout and peer-close failures retain their documented HTTP error classifications. A standalone reproduction built with the unmodified HTTP parser showed the blocking timeout transition to HPE_INVALID_EOF_STATE and loss of the remaining 15 bytes. Skipping the zero-length parser call delivered the full chunk and message-complete callback. Disclosure: this fix was prepared with AI assistance (Claude) and reviewed by me before submission. Constraint: esp_http_client_get_data returns raw transport result values before esp_transport_translate_error. Rejected: Keep the async-only zero-length guard | blocking transport reads also return zero on timeout. Confidence: high Scope-risk: moderate Directive: Do not pass a transient zero-length transport read to the HTTP parser as EOF. Tested: standalone blocking mid-chunk timeout reproduction; source-only duplicate-comment cleanup; git diff --check Not-tested: hardware TLS transport integration Signed-off-by: yi chen <94xhn1@gmail.com>
94xhn
force-pushed
the
fix/http-client-blocking-read-timeout
branch
from
July 14, 2026 14:13
0e76dec to
412114a
Compare
Contributor
Author
|
Updated in 412114a. The commit message now describes the actual outcome: later bytes are discarded and silent truncation can be treated as success; it no longer claims the blocking client loops forever. I also removed the duplicate explanatory comment. |
Collaborator
|
sha=412114a9939d3cf91f8bb8501ad5f09a2fef4240 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related bugs in
components/esp_http_client/esp_http_client.c's handling ofesp_transport_read()'s timeout return value (ERR_TCP_TRANSPORT_CONNECTION_TIMEOUT == 0), found while reading the read/retry path.1. Blocking-mode read timeout permanently wedges the HTTP parser
esp_http_client_get_data():esp_transport_read()(tcp_read()/ssl_read()incomponents/tcp_transport/transport_ssl.c) returns0(ERR_TCP_TRANSPORT_CONNECTION_TIMEOUT) wheneveresp_transport_poll_read()times out with no data ready. This is not an async-only condition — it happens in blocking mode too, any time the peer pauses longer thanclient->timeout_msmid-response (slow server, WiFi hiccup, etc). The guard above only skips the parser call whenclient->is_async && rlen == 0, so a blocking-mode client callshttp_parser_execute(parser, settings, buf, /*len=*/0)whenever this happens.http_parser_execute(..., len=0)is only valid to signal genuine end-of-stream (http_parser.c, statess_body_identity_eof/s_dead/s_start_*). In any other state — which is exactly the state a mid-response stall leaves the parser in (mid-header, mid-chunk, etc.) — it setsHPE_INVALID_EOF_STATE. Every subsequent call tohttp_parser_execute()starts with:so the parser is permanently wedged for the rest of that connection: further reads can keep succeeding (
rlen > 0), but the bytes are silently dropped by the dead parser —data_processnever advances. Inesp_http_client_perform()'s body-reading loop (while (data_process < content_length) { ret = get_data(); if (ret <= 0) break; }),retstays positive whiledata_processnever reachescontent_length, so the loop never terminates — a permanent hang of the calling task on a normal blocking client, triggered by a single transient read stall.Fix: skip the parser call whenever
rlen == 0, in any mode.rlen == 0fromesp_transport_read()is never a legitimate zero-byte success — a clean close is reported as-1(ERR_TCP_TRANSPORT_CONNECTION_CLOSED_BY_FIN), not0— so there's no case where thelen==0call is actually wanted at this call site. (A different call site,esp_http_client_read()at line ~1467, already does the correct, narrowly-scoped thing: it explicitly callshttp_parser_execute(..., 0)only whenrlen == ERR_TCP_TRANSPORT_CONNECTION_CLOSED_BY_FIN, i.e. genuine EOF.)2. Dead error-classification branches (wrong constant family)
esp_http_client_perform(), in both the chunked and content-length body-reading loops:rethere is the raw, untranslated value returned byesp_http_client_get_data()— i.e.esp_transport_read()'s ownERR_TCP_TRANSPORT_*enum (include/esp_transport.h:0/-1/-2/-3). It's compared againstESP_ERR_TCP_TRANSPORT_*— the translatedesp_err_tcodes (0xe001/0xe002), only ever produced byesp_transport_translate_error(). Sinceretcan never equal0xe001/0xe002, both branches are dead code, and every read timeout or clean close during body reading gets reported as the genericESP_ERR_HTTP_INCOMPLETE_DATA— contradicting the documented return values inesp_http_client.h("ESP_ERR_HTTP_READ_TIMEOUTif read operation times out ...ESP_ERR_HTTP_CONNECTION_CLOSEDif server closes the connection").This same file already gets this right elsewhere, e.g.
esp_http_client_read():Fix: compare against the correct, untranslated
ERR_TCP_TRANSPORT_CONNECTION_TIMEOUT/ERR_TCP_TRANSPORT_CONNECTION_CLOSED_BY_FINconstants (already in scope via the existingesp_transport.hinclude), matching the convention used everywhere else in this file.Testing
I verified issue #1 (the parser wedge, the more serious of the two) with a standalone reproduction built against the unmodified
components/http_parser/http_parser.c/.h(pure C, no other esp-idf dependencies):14\r\n, i.e. 20 bytes) plus only the first 5 bytes of that chunk's data — exactly the parser state left behind by a partial TCP read.esp_http_client_get_data()does today on a blocking-mode timeout): callhttp_parser_execute(parser, settings, buf, 0). Result:HTTP_PARSER_ERRNO(parser) == HPE_INVALID_EOF_STATE. Feeding the remaining 15 chunk bytes + the terminating0\r\n\r\nafterward parses0bytes;on_bodyfires only once (5 bytes captured instead of 20),on_message_completenever fires.len==0call is simply never issued mid-chunk): the same remaining bytes parse fully;on_bodyfires twice for a total of 20 bytes,on_message_completefires once. Full output of both runs available on request.Issue #2 is a straightforward constant-family mismatch, confirmed by reading
esp_transport.h(ERR_TCP_TRANSPORT_CONNECTION_TIMEOUT == 0vsESP_ERR_TCP_TRANSPORT_CONNECTION_TIMEOUT == 0xe001) and cross-checking against the three other call sites in the same file that already compareret/rlenagainst the correct, untranslated constants.I don't have a full on-target/QEMU or mocked-transport test environment set up for
esp_http_clientin my current setup, so I can't attach atest_apps-level run exercising the real transport + timeout path; thehttp_parser-only harness above isolates and confirms the actual state-machine defect this fix addresses. Happy to add atest_appscase if there's an existing pattern for mocking a stalling transport that I should follow.Generative AI
I used generative AI tools (Claude) when creating this PR, but a human has checked the code and is responsible for the code and the description above.
Note
Medium Risk
Touches core HTTP read/parse and error paths used by blocking
esp_http_client_perform(); fixes serious hang/misreporting bugs but changes observable error codes on timeout/close during body reads.Overview
Fixes two bugs in how
esp_http_clienthandlesesp_transport_read()returning0(read poll timeout).In
esp_http_client_get_data(),http_parser_execute(..., 0)is no longer called whenrlen == 0. The old guard only skipped that in async mode, so blocking clients could wedge the HTTP parser (HPE_INVALID_EOF_STATE) on a mid-response stall and hangesp_http_client_perform().In
esp_http_client_perform()body-read loops (chunked and content-length), incomplete-read handling now comparesretagainst rawERR_TCP_TRANSPORT_*values instead of translatedESP_ERR_TCP_TRANSPORT_*codes, so timeouts and clean FIN closes surface asESP_ERR_HTTP_READ_TIMEOUTandESP_ERR_HTTP_CONNECTION_CLOSEDinstead of alwaysESP_ERR_HTTP_INCOMPLETE_DATA.Reviewed by Cursor Bugbot for commit 412114a. Bugbot is set up for automated code reviews on this repo. Configure here.