Repository navigation
fix: drain final response buffers before completing - #497
atomicdeploy wants to merge 1 commit into
Conversation
|
@atomicdeploy please open an issue first describing the bug you saw and an MRE to reproduce it. Ideally written similar to a project exemple. |
ESPAsyncWebServer 3.12.1 marks a response ended as soon as its last piece is read from the source. When the TCP queue then refuses that piece (tcp_write ERR_MEM, likely with the ESP-01's 536-byte segments), the next ACK closes the connection with the piece still in the send buffer: the row got a clean close 439 bytes short of the row image, three offers in a row. patch_asyncweb.py now keeps the source's end apart from the response's and takes an in-flight credit only for a pass that sent bytes. Every anchor must occur exactly once. The test runs the patches on the pinned v3.12.1 excerpts. Upstream PR ESP32Async/ESPAsyncWebServer#497 is the same bug. Refs #631
Review of PR #497 — "fix: drain final response buffers before completing"
1. The reported bug — is it real?Yes, it's a real, genuine bug. A user's board download stalled at byte Root cause, traced through the code: In
The "4th try works" randomness and the "block lifted after 1 minute instead of 10" are consistent: it depends on transient lwIP buffer/window pressure at the moment the final Corroborating evidence in this repo's own history: commit 747223f ("fix: AsyncAbstractResponse might loose part of send buffer", closes #315) already fixed the header side of this exact class of bug (partial Secondary real defects the PR names, which I verified in the code:
I reproduced the bug by extracting the actual 2. The fix — is it correct?The core idea is correct and the implementation is sound. The PR:
I ran the patched code through the full harness: all cases pass under both credit configurations — partial/zero header and body adds, zero TCP window, partial final ACKs, polls, empty/known-length/unknown-length/chunked/224 KiB bodies with byte-exact reconstruction, One subtlety I checked carefully — the chunked-terminator accounting: in the Another subtlety — Edge case — empty body / immediate EOF: Edge case — who else writes to the client outside the response? I audited 3. Impacts of the fixPositive:
Behavioral changes to be aware of (all benign, but worth noting):
Not addressed by this PR (pre-existing, out of scope):
4. The testThe PR adds
That said, the harness itself is technically well-built: it compiles the actual 5. Verdict
|
mathieucarbou
left a comment
There was a problem hiding this comment.
reproducing in real life is very hard since the window when the issue cam happen is small - so it takes external conditions that are hard to control in test.
but to me the fix makes sense - we should wait for all the acks to be received I think before considering closing the connection.
There was a problem hiding this comment.
I am dismissing my approval. I found some issues.
- I am able to reproduce: I crafted a
ResponseTailTruncation_PR497.inoexample that is applying some memory pressure at key places.
- Without this fix: the responses are always truncated indeed.
- With the fix: the responses are still truncated 50% of the time.
-
I didn't find a proper deterministic way to test and validate
-
Even if it makes sense IMO from an API perspective to not close until we can be sure that all the bytes are sent, the problem is that receiving acks depends on the target host and it is possible that we did this speed improvement to close connections as fast as we can and avoid dealing with timing out waiting for acks.
-
Also, the AI-generated test in the test folder would need to be reworked and removed: we don't accept such AI slop which is unreadable and cannot be maintained (== garbage). An example need to be created so that we can run it on esp32 (under specific circumstances if needed).
-
lwip will still send the bytes avec after close is called - but we still need to fix the states to avoid truncation and too early close when we still hold some bytes. Once accepted by lwip they will be sent even after a close
This PR definitely needs more discussions and work.
There is way to validate at the moment that this fix is the right one for the bug you saw and if right, what's the impact one request speed now and ack timeout.
|
Closing in favor of #499 |
Summary
Keep
AsyncAbstractResponse's final local buffer alive until its bytes have actually been admitted to the TCP queue, and finish only after admitted header/body bytes have been acknowledged.The current implementation marks
RESPONSE_ENDwhile filling the final source buffer. A shortAsyncClient::add()can leave a tail in_send_buffer; the next request ACK then considers the response finished and closes it. Partial body adds also bypass_writtenLengthaccounting, while headers can be counted twice.Changes
RESPONSE_TRY_AGAINcredit fix and avoid spending a header credit when zero bytes were admitted.Reproduction and verification
Prepared against exact upstream main
4d4eaca1833c3c11940a988fc8a6c0bdf734bbd2.tests/run-response-backpressure.mjsextracts and compiles the actualwrite_send_buffsimplementation fromsrc/WebResponses.cppwith small transport/source doubles. It is not a separately rewritten response algorithm. Requires Node.js and a C++17g++(orCXX).The optional
--baseline SOURCE --output EXECUTABLEmode verifies the pinned original source hash before reproduction. The original method fails deterministically with an eight-byte final source buffer, one admitted body byte and seven pending bytes while already exposingRESPONSE_END. Both in-flight-credit configurations reproduce the failure. Both patched configurations pass:RESPONSE_TRY_AGAIN, failed sends and invalid sources.This is focused host/mocked-transport evidence, not a full Arduino/lwIP build, packet-level measurement, hardware/Wi-Fi soak or proof that every reported network failure has this cause. No installed SDK was modified; this upstream patch follows a downstream immutable-response workaround and its reproducible short-add regression.