Skip to content

fix(transport): run the HTTP/1.1 fallback once, whichever branch a request took - #110

Open
burruplambert wants to merge 1 commit into
sardanioss:mainfrom
burruplambert:fallback-once
Open

fix(transport): run the HTTP/1.1 fallback once, whichever branch a request took#110
burruplambert wants to merge 1 commit into
sardanioss:mainfrom
burruplambert:fallback-once

Conversation

@burruplambert

Copy link
Copy Markdown
Contributor

In auto mode, presets with H3 support go through raceH3H2, which carried its own copies of the two HTTP/1.1 fallbacks doAuto already has. When one of those internal fallbacks failed, doAuto saw an ordinary error and ran its own fresh connection fallback on top. Against a failing host an H3 capable preset made two HTTP/1.1 attempts where a preset without H3 support made one, and a request cut off on an ALPN negotiated HTTP/1.1 connection was quietly re-sent for one family of presets and returned as an error for the other. The auto negotiation docs describe one fallback, and retries are what WithRetry is for.

raceH3H2 now does what its name says: race the probes, make one attempt on the winner, return what that attempt returned, including an ALPNMismatchError with its live TLS connection. doAuto owns every fallback, and both of its branches make identical attempts.

Behaviour change on failing hosts only:

  • one fewer HTTP/1.1 attempt before the error comes back (H3 capable presets: 3 connections instead of 4 against a host that drops every connection; presets without H3: 2, unchanged)
  • a failed request on a live ALPN negotiated HTTP/1.1 connection is returned to the caller instead of being retried on a fresh connection (presets without H3 already behaved this way)

Requests that succeed today succeed the same way.

Tests in transport/fallback_once_test.go run against in-process servers and count accepted TCP connections. A server that drops every connection: firefox-148 makes 2 attempts and chrome-146 makes 3, where chrome-146 previously made 4. An http/1.1 only server that closes without answering: both presets make exactly 1 connection and the error is returned, where chrome-146 previously made 2.

…branch a request took

In auto mode, presets with H3 support go through raceH3H2, and raceH3H2
carried its own copies of the two fallbacks doAuto already has: HTTP/1.1 on
the connection that ALPN downgraded, then HTTP/1.1 on a fresh connection. When
one of those internal fallbacks failed, doAuto saw an ordinary (non-ALPN) error
and ran its own fresh-connection fallback on top. So against a failing host an
H3-capable preset made two HTTP/1.1 attempts where a preset without H3 support
made one, and a request whose ALPN-negotiated HTTP/1.1 exchange was cut off
was quietly re-sent on a new connection for one family of presets and returned
as an error for the other. None of that is documented (the auto-negotiation
page describes one fallback), and retries are what WithRetry is for.

raceH3H2 now does what its name says: race the probes, make one attempt on the
winner, return whatever that attempt returned, including an *ALPNMismatchError
with its live TLS connection. doAuto owns every fallback. Its existing ALPN
handler right after the race call, which was unreachable in practice because
raceH3H2 consumed the ALPN error itself, now serves those, and its bottom
fresh-connection fallback runs once. Both doAuto branches make identical
attempts. The ProtocolAuto "nothing learned" signal introduced by the previous
commit is gone with the fallback that needed it.

Behaviour change, deliberately, on failing hosts only:
  - one fewer HTTP/1.1 attempt before the error comes back (H3-capable
    presets: 3 connections instead of 4 against a host that drops every
    connection; presets without H3: 2, unchanged);
  - a failed request on a live ALPN-negotiated HTTP/1.1 connection is returned
    to the caller instead of being retried on a fresh connection (1 connection
    instead of 2; presets without H3 already behaved this way).
Requests that succeed today succeed the same way. Anyone relying on the
accidental second attempt has WithRetry, which is deliberate and
preset-independent.

Tests (transport/fallback_once_test.go), against real in-process servers,
counting accepted TCP connections:
  - TestAutoFreshH1FallbackRunsOnce: a server that drops every connection;
    firefox-148 sees 2 attempts and chrome-146 sees 3 (one extra for the H2
    probe). Previously chrome-146 made 4.
  - TestAutoALPNDowngradeFailureIsNotRetried: an http/1.1-only server that
    hijacks and closes without answering; both presets see exactly 1
    connection, the error is returned, nothing is cached. Previously
    chrome-146 made 2 and succeeded on the second.
The protocol-cache tests from the previous commit still pass unchanged.
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

@burruplambert is attempting to deploy a commit to the sardanioss' projects Team on Vercel.

A member of the Team first needs to authorize it.

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.

1 participant