Skip to content

Don't spend an in-flight credit when nothing was sent - #488

Open
MitchBradley wants to merge 1 commit into
ESP32Async:mainfrom
MitchBradley:FixInFlightCreditStall
Open

MitchBradley wants to merge 1 commit into
ESP32Async:mainfrom
MitchBradley:FixInFlightCreditStall

Conversation

@MitchBradley

Copy link
Copy Markdown

Problem

With ASYNCWEBSERVER_USE_CHUNK_INFLIGHT enabled (the default), a chunked or unknown-length response can stall permanently if its source isn't ready on a couple of consecutive calls.

AsyncAbstractResponse::write_send_buffs() takes an in-flight credit at the end of the content stage on every call:

    _in_flight += payloadlen;
    --_in_flight_credit;  // take a credit

It does this even when _fillBufferAndProcessTemplates() returned RESPONSE_TRY_AGAIN and payloadlen is 0. Credits come back only with acks of sent data (len > 0); polls never return one. So:

  1. The response starts with 2 credits.
  2. Each call that finds the source empty spends one, and nothing is sent, so no ack will ever return it.
  3. After two such calls, _in_flight_credit is 0. Every later poll hits the !_in_flight_credit early return, the user callback is never called again, and the response hangs until the connection times out.

This hits any response whose producer can be momentarily slower than the connection drains: a chunked response fed by another task, for example.

How it showed up

In FluidNC, HTTP commands run on another task and stream their output into a small buffer that a chunked response drains, returning RESPONSE_TRY_AGAIN while the buffer is empty. When the network side drained faster than the command produced output, the response stalled after its first couple of empty fills. The command task then gave up on the full, undrained buffer, and clients received truncated replies (the first 2 KB of a 9 KB JSON settings listing).

It's easiest to reproduce on a host build with a loopback-fast transport where acks are immediate. On ESP32 the same sequence needs a producer that falls behind the TCP drain, so it's intermittent there.

Fix

Take a credit, and count _in_flight, only when payloadlen is nonzero. An empty call puts nothing in flight, so it shouldn't consume flight credit. Retries from polls then keep working until the source has data, and normal ack pacing resumes from there.

Testing

On a host (Emscripten) build of FluidNC, using a custom AsyncTCP transport with immediate acks and ASYNCWEBSERVER_USE_CHUNK_INFLIGHT=1, fetching the ~9 KB settings listing over a chunked response:

  • before: 8 of 9 replies truncated (2047 or 3000 bytes)
  • after: 15 of 15 replies complete and valid, across five fresh starts; FluidNC's other HTTP and WebSocket tests also pass

Not yet tested on ESP32 hardware with lwIP. The change only affects calls that write zero bytes, so it can't increase data in flight.

🤖 Generated with Claude Code

With ASYNCWEBSERVER_USE_CHUNK_INFLIGHT, write_send_buffs() took an
in-flight credit on every call that reached the content stage, even when
the source had no data yet (RESPONSE_TRY_AGAIN) and nothing was written.
Credits come back only with acks of sent data, never from polls, so a
response whose source is empty on two such calls runs out of credits.
From then on every call is ignored and the response stalls for good.

Take a credit only when data was actually put in flight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 9, 2026 21:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused change correctly preserves retry capability without altering accounting for transmitted data.

0 open findings

What changed in this PR

Prevents stalled chunked or unknown-length responses when their source temporarily has no data.

Changes:

  • Consume in-flight credit only when bytes are actually sent.
  • Document why zero-byte retries must retain credit.
File Description
src/​WebResponses.cpp Guards in-flight accounting against zero-byte sends.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@mathieucarbou mathieucarbou added the Type: Bug Something isn't working label Oct 9, 2026
@mathieucarbou
mathieucarbou requested review from willmmiles and a balanced review from Copilot October 9, 2026 21:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused change correctly preserves credits for zero-byte retries without altering normal send accounting.

0 open findings

🧠 Review effort: Balanced

@mathieucarbou

Copy link
Copy Markdown
Member

Thank you for this bug fix - I agree with the analysis we might have missed this use case!

I will try to test this weekend.

@mathieucarbou

Copy link
Copy Markdown
Member

@willmmiles : pinging you to approve this small fix also. thanks!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status: Pending Merge Type: Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants