Skip to content

fix(netty): fail fast when InputStream body cannot be reset - #2312

Merged
hyperxpro merged 3 commits into
AsyncHttpClient:mainfrom
arimu1:fix/1973-consumed-stream-http1-fail-fast
Aug 16, 2026
Merged

fix(netty): fail fast when InputStream body cannot be reset#2312
hyperxpro merged 3 commits into
AsyncHttpClient:mainfrom
arimu1:fix/1973-consumed-stream-http1-fail-fast

Conversation

@arimu1

@arimu1 arimu1 commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • On HTTP/1, a consumed InputStream request body used to log a warning and return, leaving NettyRequestSender.writeRequest without completing the future so the request hung until timeout (Failed stream request hangs until timeout #1973). Headers used channel.write with no flush; the peer has seen nothing.
  • Throw IOException instead so writeRequest aborts the future. A failed reset() on a closed markable stream (for example BufferedInputStream after WriteProgressListener closes it) is treated the same way, so retries fail fast instead of hanging or throwing only "Stream closed".
  • HTTP/1 100 Continue now uses the same bodyWasDeferred guard as HTTP/2, so an unsolicited 100 (RFC 9110 15.2.1) does not replay a body that was already sent.
  • Add regression coverage for write, writeHttp2, the writeRequest abort path, and a real request replay.

Fixes #1973

AI disclosure

Composer 2.5 on behalf of arimu1 (Cursor harness). Commit includes Co-Authored-By: Composer 2.5 per AGENTS.md.

Test plan

  • ./mvnw -pl client -Dtest=NettyInputStreamBodyTest,Continue100InterceptorTest,NettyRequestSenderConsumedBodyRetryTest test (JDK 17)

Made with Cursor

arimu1 and others added 2 commits August 15, 2026 10:10
HTTP/1 path silently returned after a warn when a consumed non-resettable
InputStream body was reused, leaving a half-sent request that hung until
timeout. Throw IOException so sendHttpRequest aborts the future (matches
existing HTTP/2 behavior).

Composer 2.5 on behalf of arimu1

Fixes AsyncHttpClient#1973

Co-Authored-By: Composer 2.5 <composer@cursor.com>
Composer 2.5 on behalf of arimu1

Co-Authored-By: Composer 2.5 <composer@cursor.com>
Unsolicited HTTP/1 100 Continue was replaying bodies that were
already sent. Guard with bodyWasDeferred like HTTP/2. Treat a
failed reset() on a closed markable stream as unreplayable so
retries abort the future instead of hanging or throwing Stream
closed. Comments now cite NettyRequestSender.writeRequest.

Addresses review on AsyncHttpClient#2312.

Composer 2.5 on behalf of arimu1

Co-Authored-By: Composer 2.5 <composer@cursor.com>
@arimu1

arimu1 commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the CHANGES_REQUESTED review (commit da62c2fd1):

  1. HTTP/1 unsolicited 100 -- Continue100Interceptor now has the same bodyWasDeferred guard as HTTP/2. We only schedule writeRequest when the body was actually deferred for Expect: 100-continue.
  2. markSupported() / closed stream -- replayConsumedStream still tries reset() when mark is supported, but wraps a failed reset (closed BufferedInputStream, no prior mark()) in a clear IOException so the user does not see a bare "Stream closed".
  3. Unsolicited 100 + resettable body / second LastHttpContent -- closed by the same bodyWasDeferred guard.
  4. sendHttpRequest -- comments and the PR description now cite NettyRequestSender.writeRequest (headers ~732, catch/abort ~751-754).
  5. Throw-comment nits -- HTTP/1 comment now states that headers used channel.write with no flush (peer has seen nothing) and that the real hang is the future never completing. ASCII only (-- / commas, no em dash).
  6. HTTP/2 issue number -- restored #2160 on the half-open-stream comment. #1973 stays on the HTTP/1 consumed-body path.
  7. Tests -- NettyRequestSender.writeRequest is driven twice (unconsumed, then consumed) and asserts the future fails within 2s. Integration replay of a real POST covers the #1973 hang. Also covered: markSupported()==true reset-fail (BufferedInputStream after close), writeHttp2 sibling, stream closed after the first write. No exact exception-message asserts. Non-markable branch uses InputStream.nullInputStream() (JDK 11).

Happy to follow up if anything still looks off.

@hyperxpro
hyperxpro merged commit 793aae9 into AsyncHttpClient:main Aug 16, 2026
13 checks passed
@hyperxpro

Copy link
Copy Markdown
Member

Thanks a lot!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failed stream request hangs until timeout

2 participants