Skip to content

Allow clients to select ALPN protocols instead of forcing HTTP/2 - #161

Merged
rmehta19 merged 3 commits into
google:mainfrom
anushka567:alpn-next-protos
Sep 30, 2026
Merged

rmehta19 merged 3 commits into
google:mainfrom
anushka567:alpn-next-protos

Conversation

@anushka567

Copy link
Copy Markdown
Contributor

Description

This PR carries forward the changes from #159 (by @sruthi-talluri) to allow clients to select ALPN protocols instead of forcing HTTP/2, and addresses the review feedback on that PR.

Background

GetTLSConfigurationForClient has hardcoded NextProtos to {"h2"} since #72, which was added when s2a-go was only used for gRPC. The HTTP entry points added later (NewTLSClientConfigFactory and NewS2ADialTLSContextFunc) inherited that default, so an HTTP/1.1 client gets a connection that has negotiated h2 via ALPN. net/http then writes HTTP/1.1 onto it and the first response fails to parse:

net/http: HTTP/1.x transport connection broken: malformed HTTP response
"\x00\x00$\x04\x00\x00\x00\x00\x00\x00\x05\x00\x10..."

which is an HTTP/2 SETTINGS frame.

Changes

  1. ALPN Selection:

    • Adds ClientOptions.NextProtos (applied when a client tls.Config is built via NewS2ADialTLSContextFunc and NewTLSClientConfigFactory).
    • Adds TLSClientConfigOptions.NextProtos as a per-Build() override.
    • Falls back to {"h2"} when unset, and strictly enforces HTTP/2 for gRPC client handshakes.
  2. Addressed Review Feedback from Allow clients to select ALPN protocols instead of forcing HTTP/2 #159:

    • Replaced manual slice append with slices.Clone(nextProtos) in internal/v2/tlsconfigstore/tlsconfigstore.go.
    • Cleaned up doc comment on nextProtosOrDefault.
    • Added unit tests for NewTLSClientConfigFactory with custom NextProtos in s2a_test.go.
    • Added end-to-end HTTP/1.1 negotiation test in s2a_e2e_test.go.

Co-authored-by: Sruthi Talluri sruthitalluri@google.com

sruthi-talluri and others added 2 commits September 18, 2026 03:34
GetTLSConfigurationForClient has hardcoded NextProtos to {"h2"} since google#72,
which was added in 2022 when s2a-go was only used for gRPC. The HTTP entry
points added later, NewTLSClientConfigFactory and NewS2ADialTLSContextFunc,
inherited that default, so an HTTP/1.1 client gets a connection that has
negotiated h2 via ALPN. net/http then writes HTTP/1.1 onto it and the first
response fails to parse:

  net/http: HTTP/1.x transport connection broken: malformed HTTP response
  "\x00\x00$\x04\x00\x00\x00\x00\x00\x00\x05\x00\x10..."

which is an HTTP/2 SETTINGS frame. There is currently no way to avoid this
other than rebuilding the tls.Config by hand after Build returns.

This adds ClientOptions.NextProtos, applied whenever a client tls.Config is
built, and TLSClientConfigOptions.NextProtos as a per-Build override.
NewS2ADialTLSContextFunc goes through NewTLSClientConfigFactory, so it picks
the option up as well.

Behavior is unchanged when neither is set: nextProtosOrDefault falls back to
{"h2"}. The gRPC ClientHandshake path in internal/v2/s2av2.go passes nil
explicitly, since that transport requires HTTP/2 and must not be overridable.
The returned slice is copied so a later mutation by the caller cannot alter a
live tls.Config.
- Use slices.Clone in nextProtosOrDefault and simplify doc comment
- Add unit tests for TLSClientConfigFactory with custom NextProtos in s2a_test.go
- Add end-to-end HTTP/1.1 test in s2a_e2e_test.go
Comment thread internal/v2/s2av2_e2e_test.go Outdated
Comment thread s2a.go
@rmehta19

Copy link
Copy Markdown
Collaborator

Can we manually E2E test this? We have an example folder in the top level directory which may contain some useful components for testing

@anushka567

Copy link
Copy Markdown
Contributor Author

Thanks @rmehta19, addressed the nits. I ran a manual E2E with every component as a separate OS process: the standalone fake S2A , a plain crypto/tls + net/http server offering ALPN [h2, http/1.1], and a small client using only the public s2a package (NewS2ADialTLSContextFunc). Testing details:

  • main (fe5acd2), default options: server saw ALPN offer [h2]; client failed with malformed HTTP response (the bug this PR fixes).
  • PR (f59ab3d), default options: identical result, so the default is unchanged.
  • PR with NextProtos: []string{"http/1.1"}: server saw offer [http/1.1]; 200 OK, resp.Proto=HTTP/1.1, negotiated http/1.1.

gRPC: I couldn't wire example/client , example/server to the fake as-is. None of the three binaries call flag.Parse(), so --port, --s2a_addr are ignored and the defaults don't line up (fake :8008 vs examples 0.0.0.0:61365). The gRPC path is still covered by the existing e2e tests (e.g. TestV2EndToEndUsingFakeS2AOverTCP).

@rmehta19
rmehta19 merged commit 0d935c3 into google:main Sep 30, 2026
7 checks passed
anushka567 added a commit to anushka567/gcsfuse that referenced this pull request Oct 6, 2026
s2a-go v0.1.11 (google/s2a-go#161) adds ClientOptions.NextProtos, so a
client can choose the ALPN protocols offered during the S2A TLS handshake
instead of always offering "h2". Upgrade to v0.1.11 and set
ClientOptions.NextProtos = []string{"http/1.1"} when client-protocol is
http1, replacing the workaround that overwrote tlsConfig.NextProtos after
factory.Build.

With ALPN configured via ClientOptions, http1 and http2 can share a single
S2A dialer helper (newS2ADialTLSContext) that opens its TCP connection with
the base net.Dialer. Previously http2 called s2a.NewS2ADialTLSContextFunc,
which dials with a zero-value net.Dialer, and http.Transport ignores
DialContext for HTTPS once DialTLSContext is set; sharing the helper keeps
--experimental-local-socket-address and --enable-http-dns-cache working for
both http1 and http2 when S2A is enabled.
anushka567 added a commit to anushka567/gcsfuse that referenced this pull request Oct 6, 2026
s2a-go v0.1.11 (google/s2a-go#161) adds ClientOptions.NextProtos, so a
client can choose the ALPN protocols offered during the S2A TLS handshake
instead of always offering "h2". Upgrade to v0.1.11 and set
ClientOptions.NextProtos = []string{"http/1.1"} when clientProtocol is
http1, replacing the workaround that overwrote tlsConfig.NextProtos after
factory.Build.

With ALPN configured via ClientOptions, http1 and http2 can share a single
S2A dialer helper (newS2ADialTLSContext) that opens its TCP connection with
the base net.Dialer. Previously http2 called s2a.NewS2ADialTLSContextFunc,
which dials with a zero-value net.Dialer, and http.Transport ignores
DialContext for HTTPS once DialTLSContext is set; sharing the helper keeps
--experimental-local-socket-address and --enable-http-dns-cache working for
both http1 and http2 when S2A is enabled.
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.

4 participants