From cf08ad9adc5eeda386350af4fb9958fd3107c625 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 26 Oct 2026 00:06:38 +0000 Subject: [PATCH] Disable extended CONNECT by default Browsers take a server advertising extended CONNECT as support for WebSockets over HTTP/2, which fails when the server's WebSocket handling does not support it. Go disabled it by default for this reason. Port golang.org/x/net/http2 0a5dcdd8 (CL 641475, golang/go#71128): extended CONNECT is off unless GODEBUG contains http2xconnect=1. Also port the check from 9a518991 (CL 610977) that a535bcd left out: while it is off, the server rejects requests that use :protocol. Co-Authored-By: Claude Opus 5.5 --- http2/http2.go | 22 ++++++--- http2/server.go | 5 +++ http2/server_extended_connect_test.go | 64 +++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 http2/server_extended_connect_test.go diff --git a/http2/http2.go b/http2/http2.go index b77d8917..d30bb70c 100644 --- a/http2/http2.go +++ b/http2/http2.go @@ -33,11 +33,19 @@ import ( ) var ( - VerboseLogs bool - logFrameWrites bool - logFrameReads bool - inTests bool - disableExtendedConnectProtocol bool + VerboseLogs bool + logFrameWrites bool + logFrameReads bool + inTests bool + + // Enabling extended CONNECT by causes browsers to attempt to use + // WebSockets-over-HTTP/2. This results in problems when the server's websocket + // package doesn't support extended CONNECT. + // + // Disable extended CONNECT by default for now. + // + // Issue #71128. + disableExtendedConnectProtocol = true ) func init() { @@ -50,8 +58,8 @@ func init() { logFrameWrites = true logFrameReads = true } - if strings.Contains(e, "http2xconnect=0") { - disableExtendedConnectProtocol = true + if strings.Contains(e, "http2xconnect=1") { + disableExtendedConnectProtocol = false } } diff --git a/http2/server.go b/http2/server.go index 9173f1c4..40f66cd5 100644 --- a/http2/server.go +++ b/http2/server.go @@ -1993,6 +1993,11 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res protocol: f.PseudoValue("protocol"), } + // extended connect is disabled, so we should not see :protocol + if disableExtendedConnectProtocol && rp.protocol != "" { + return nil, nil, streamError(f.StreamID, ErrCodeProtocol) + } + isConnect := rp.method == "CONNECT" if isConnect { if rp.protocol == "" && (rp.path != "" || rp.scheme != "" || rp.authority == "") { diff --git a/http2/server_extended_connect_test.go b/http2/server_extended_connect_test.go new file mode 100644 index 00000000..04f0bc6c --- /dev/null +++ b/http2/server_extended_connect_test.go @@ -0,0 +1,64 @@ +package http2 + +import ( + "os" + "os/exec" + "testing" + "time" + + http "github.com/bogdanfinn/fhttp" +) + +// Extended CONNECT is off unless GODEBUG=http2xconnect=1 at startup +// (golang/go#71128). GODEBUG is read at init, so each setting runs in a new +// process. +func TestServerExtendedConnect(t *testing.T) { + if want := os.Getenv("FHTTP_TEST_XCONNECT"); want != "" { + testServerExtendedConnect(t, want == "on") + return + } + for _, tt := range []struct{ godebug, want string }{ + {"", "off"}, + {"http2xconnect=0", "off"}, + {"http2xconnect=1", "on"}, + } { + cmd := exec.Command(os.Args[0], "-test.run=^TestServerExtendedConnect$") + cmd.Env = append(os.Environ(), "FHTTP_TEST_XCONNECT="+tt.want, "GODEBUG="+tt.godebug) + if out, err := cmd.CombinedOutput(); err != nil { + t.Errorf("GODEBUG=%q: %v\n%s", tt.godebug, err, out) + } + } +} + +// testServerExtendedConnect checks that the server advertises extended CONNECT +// and accepts requests with :protocol exactly when it is enabled. +func testServerExtendedConnect(t *testing.T, enabled bool) { + gotProtocol := make(chan string, 1) + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { + gotProtocol <- r.Header.Get(":protocol") + }) + defer st.Close() + + advertised := false + st.greetAndCheckSettings(func(s Setting) error { + advertised = advertised || s.ID == SettingEnableConnectProtocol && s.Val == 1 + return nil + }) + if advertised != enabled { + t.Fatalf("SETTINGS_ENABLE_CONNECT_PROTOCOL advertised = %v, want %v", advertised, enabled) + } + + st.bodylessReq1(":method", "CONNECT", ":protocol", "websocket") + if !enabled { + st.wantRSTStream(1, ErrCodeProtocol) + return + } + select { + case p := <-gotProtocol: + if p != "websocket" { + t.Fatalf(":protocol = %q, want %q", p, "websocket") + } + case <-time.After(5 * time.Second): + t.Fatal("handler was not called for an extended CONNECT request") + } +}