Skip to content

Fix H1 pool retry when recycled connection got close_notify after response#650

Merged
haga-rak merged 2 commits into
mainfrom
dev/fix-h11-tls-bc-close
May 4, 2026
Merged

Fix H1 pool retry when recycled connection got close_notify after response#650
haga-rak merged 2 commits into
mainfrom
dev/fix-h11-tls-bc-close

Conversation

@haga-rak
Copy link
Copy Markdown
Owner

@haga-rak haga-rak commented May 4, 2026

Summary

Production trace: HTTP/2 trace with Http11ConnectionPool + BouncyCastle showed the third request landing on a connection the upstream had already closed (TLS close_notify + FIN immediately after response #2, no Connection: close header). The pool blindly recycled the dead connection and the request surfaced as 528 instead of retrying on a fresh connection.

What changed

  • Exchange.RecycledConnection flag, set by Http11ConnectionPool.Send when pulling from the pool. Lets the failure path tell "stale pooled connection died on us" (safe to relaunch) apart from "fresh connection failed immediately" (genuine upstream issue, must surface as 528 instead of looping).
  • Http11PoolProcessing response-read catch and Http11ConnectionPool.Send outer catch both convert IOException / SocketException to ConnectionCloseException("Relaunch") when RecycledConnection && ResponseHeaderStart == default. Covers both response-read and request-write failure paths.
  • The interim-response sniff at Http11PoolProcessing was indexing buffer.Buffer.AsSpan(0, HeaderLength) immediately after GetNext, which crashed with ArgumentOutOfRangeException when GetNext returned the close_notify sentinel HeaderLength = -1. Skip the sniff on CloseNotify.
  • Http11ConnectionPool.Send cleanup branch now disposes ReadStream and nulls exchange.Connection for ConnectionCloseException | TlsFatalAlert | IOException | SocketException, not just the first.
  • DisposeEventNotifierStream.ReadAsync async overload now sets Faulted = true on exception, mirroring the sync overload.

Reproducer

Http11PoolCloseNotifyAfterResponseTests stands up a raw TLS listener that mimics the trace (two responses then SslStream.ShutdownAsync + close), pipes through a real Fluxzy proxy on both BouncyCastle and OS engines, and asserts the third request returns 200 and triggered a fresh upstream accept.

haga-rak added 2 commits May 4, 2026 14:53
…ponse

Broaden the relaunch predicate in Http11PoolProcessing so a recycled
connection that died while idle (TLS close_notify + FIN, then RST on
write) maps IOException/SocketException to ConnectionCloseException
when no response byte was received yet. Track RecycledConnection on
Exchange so a fresh-connection failure still surfaces as 528 instead
of looping. Also propagate Faulted=true through the async ReadAsync
path of DisposeEventNotifierStream and broaden the cleanup branch in
Http11ConnectionPool.Send so any dead-connection signal disposes the
read stream.
…er write-path failure

Two latent issues uncovered while running the previous fix under load.

1) Http11PoolProcessing's interim-response loop indexed the buffer with
   HeaderLength = -1 (the close_notify sentinel returned by GetNext) and
   threw ArgumentOutOfRangeException before the post-try CloseNotify
   branch could turn it into a relaunch. Skip the interim sniff when
   CloseNotify is set.

2) The recycled-and-no-response conversion only ran in the response-read
   try/catch. A request-write failure on a recycled connection (TCP RST
   already in flight) bypassed it entirely and surfaced as 528. Added the
   same conversion at the Http11ConnectionPool.Send catch boundary so
   both paths behave consistently.
@haga-rak haga-rak added the bug Something isn't working label May 4, 2026
@haga-rak haga-rak merged commit aa8d7db into main May 4, 2026
3 of 4 checks passed
@haga-rak haga-rak deleted the dev/fix-h11-tls-bc-close branch May 7, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant