| // Copyright 2026 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package http2_test | |
| import ( | |
| "bytes" | |
| "crypto/tls" | |
| "flag" | |
| "io" | |
| "log" | |
| "net" | |
| "net/http" | |
| "os" | |
| "strings" | |
| "sync" | |
| "testing" | |
| "testing/synctest" | |
| "time" | |
| . "golang.org/x/net/http2" | |
| "golang.org/x/net/http2/hpack" | |
| ) | |
| var stderrVerbose = flag.Bool("stderr_verbose", false, "Mirror verbosity to stderr, unbuffered") | |
| func stderrv() io.Writer { | |
| if *stderrVerbose { | |
| return os.Stderr | |
| } | |
| return io.Discard | |
| } | |
| type safeBuffer struct { | |
| b bytes.Buffer | |
| m sync.Mutex | |
| } | |
| func (sb *safeBuffer) Write(d []byte) (int, error) { | |
| sb.m.Lock() | |
| defer sb.m.Unlock() | |
| return sb.b.Write(d) | |
| } | |
| func (sb *safeBuffer) Bytes() []byte { | |
| sb.m.Lock() | |
| defer sb.m.Unlock() | |
| return sb.b.Bytes() | |
| } | |
| func (sb *safeBuffer) Len() int { | |
| sb.m.Lock() | |
| defer sb.m.Unlock() | |
| return sb.b.Len() | |
| } | |
| type serverTester struct { | |
| cc net.Conn // client conn | |
| t testing.TB | |
| h1server *http.Server | |
| h2server *Server | |
| serverLogBuf safeBuffer // logger for httptest.Server | |
| logFilter []string // substrings to filter out | |
| scMu sync.Mutex // guards sc | |
| sc *ServerConn | |
| testConnFramer | |
| callsMu sync.Mutex | |
| calls []*serverHandlerCall | |
| // If http2debug!=2, then we capture Frame debug logs that will be written | |
| // to t.Log after a test fails. The read and write logs use separate locks | |
| // and buffers so we don't accidentally introduce synchronization between | |
| // the read and write goroutines, which may hide data races. | |
| frameReadLogMu sync.Mutex | |
| frameReadLogBuf bytes.Buffer | |
| frameWriteLogMu sync.Mutex | |
| frameWriteLogBuf bytes.Buffer | |
| // writing headers: | |
| headerBuf bytes.Buffer | |
| hpackEnc *hpack.Encoder | |
| } | |
| type twriter struct { | |
| t testing.TB | |
| st *serverTester // optional | |
| } | |
| func (w twriter) Write(p []byte) (n int, err error) { | |
| if w.st != nil { | |
| ps := string(p) | |
| for _, phrase := range w.st.logFilter { | |
| if strings.Contains(ps, phrase) { | |
| return len(p), nil // no logging | |
| } | |
| } | |
| } | |
| w.t.Logf("%s", p) | |
| return len(p), nil | |
| } | |
| type serverTesterOpt string | |
| var ( | |
| optFramerReuseFrames = serverTesterOpt("frame_reuse_frames") | |
| // optNoConn indicates that newServerTester should not create a connection. | |
| // The test will create its own. | |
| optNoConn = serverTesterOpt("no_conn") | |
| optQuiet = func(server *http.Server) { | |
| server.ErrorLog = log.New(io.Discard, "", 0) | |
| } | |
| ) | |
| func newServerTester(t testing.TB, handler http.HandlerFunc, opts ...interface{}) *serverTester { | |
| t.Helper() | |
| h1server := &http.Server{} | |
| h2server := &Server{} | |
| tlsState := tls.ConnectionState{ | |
| Version: tls.VersionTLS13, | |
| ServerName: "go.dev", | |
| CipherSuite: tls.TLS_AES_128_GCM_SHA256, | |
| NegotiatedProtocol: "h2", | |
| } | |
| noConn := false | |
| for _, opt := range opts { | |
| switch v := opt.(type) { | |
| case func(*Server): | |
| v(h2server) | |
| case func(*http.Server): | |
| v(h1server) | |
| case func(*tls.ConnectionState): | |
| v(&tlsState) | |
| case serverTesterOpt: | |
| switch v { | |
| case optNoConn: | |
| noConn = true | |
| default: | |
| t.Fatalf("unhandled option %v", v) | |
| } | |
| default: | |
| t.Fatalf("unknown newServerTester option type %T", v) | |
| } | |
| } | |
| ConfigureServer(h1server, h2server) | |
| cli, srv := synctestNetPipe() | |
| cli.SetReadDeadline(time.Now()) | |
| cli.autoWait = true | |
| t.Cleanup(func() { | |
| cli.Close() | |
| srv.Close() | |
| }) | |
| st := &serverTester{ | |
| t: t, | |
| cc: cli, | |
| h1server: h1server, | |
| h2server: h2server, | |
| } | |
| st.hpackEnc = hpack.NewEncoder(&st.headerBuf) | |
| if h1server.ErrorLog == nil { | |
| h1server.ErrorLog = log.New(io.MultiWriter(stderrv(), twriter{t: t, st: st}, &st.serverLogBuf), "", log.LstdFlags) | |
| } | |
| if handler == nil { | |
| handler = serverTesterHandler{st}.ServeHTTP | |
| } | |
| t.Cleanup(func() { | |
| st.Close() | |
| time.Sleep(GoAwayTimeout) // give server time to shut down | |
| }) | |
| if noConn { | |
| return st | |
| } | |
| var connc chan *ServerConn | |
| if !wrappedAPI { | |
| connc = make(chan *ServerConn) | |
| h2server.TestSetNewConnFunc(func(sc *ServerConn) { | |
| connc <- sc | |
| close(connc) // panic if we unexpectedly get a second conn | |
| }) | |
| } | |
| go func() { | |
| h2server.ServeConn(&netConnWithConnectionState{ | |
| Conn: srv, | |
| state: tlsState, | |
| }, &ServeConnOpts{ | |
| Handler: handler, | |
| BaseConfig: h1server, | |
| }) | |
| }() | |
| if !wrappedAPI { | |
| st.sc = <-connc | |
| } | |
| st.testConnFramer = testConnFramer{ | |
| t: t, | |
| fr: NewFramer(st.cc, st.cc), | |
| dec: hpack.NewDecoder(InitialHeaderTableSize, nil), | |
| } | |
| synctest.Wait() | |
| return st | |
| } | |
| // testServerConn is a single connection to a server. | |
| type testServerConn struct { | |
| t testing.TB | |
| netconn *synctestNetConn | |
| testConnFramer | |
| } | |
| func newTestServerConn(t testing.TB, nc *synctestNetConn) *testServerConn { | |
| tc := &testServerConn{ | |
| t: t, | |
| netconn: nc, | |
| } | |
| tc.testConnFramer = testConnFramer{ | |
| t: t, | |
| fr: NewFramer(nc, nc), | |
| dec: hpack.NewDecoder(InitialHeaderTableSize, nil), | |
| } | |
| return tc | |
| } | |
| func (tc *testServerConn) writePreface() { | |
| tc.t.Helper() | |
| n, err := tc.netconn.Write([]byte(ClientPreface)) | |
| if err != nil { | |
| tc.t.Fatalf("Error writing client preface: %v", err) | |
| } | |
| if n != len(ClientPreface) { | |
| tc.t.Fatalf("Writing client preface, wrote %d bytes; want %d", n, len(ClientPreface)) | |
| } | |
| } | |
| func (tc *testServerConn) greet() { | |
| tc.t.Helper() | |
| tc.writePreface() | |
| tc.wantFrameType(FrameSettings) | |
| tc.wantFrameType(FrameWindowUpdate) | |
| tc.writeSettings() | |
| tc.wantFrameType(FrameSettings) // ACK | |
| tc.writeSettingsAck() | |
| } | |
| type netConnWithConnectionState struct { | |
| net.Conn | |
| state tls.ConnectionState | |
| } | |
| func (c *netConnWithConnectionState) ConnectionState() tls.ConnectionState { | |
| return c.state | |
| } | |
| type serverTesterHandler struct { | |
| st *serverTester | |
| } | |
| func (h serverTesterHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | |
| call := &serverHandlerCall{ | |
| w: w, | |
| req: req, | |
| ch: make(chan func()), | |
| } | |
| h.st.t.Cleanup(call.exit) | |
| h.st.callsMu.Lock() | |
| h.st.calls = append(h.st.calls, call) | |
| h.st.callsMu.Unlock() | |
| for f := range call.ch { | |
| f() | |
| } | |
| } | |
| // serverHandlerCall is a call to the server handler's ServeHTTP method. | |
| type serverHandlerCall struct { | |
| w http.ResponseWriter | |
| req *http.Request | |
| closeOnce sync.Once | |
| ch chan func() | |
| } | |
| // do executes f in the handler's goroutine. | |
| func (call *serverHandlerCall) do(f func(http.ResponseWriter, *http.Request)) { | |
| donec := make(chan struct{}) | |
| call.ch <- func() { | |
| defer close(donec) | |
| f(call.w, call.req) | |
| } | |
| <-donec | |
| } | |
| // exit causes the handler to return. | |
| func (call *serverHandlerCall) exit() { | |
| call.closeOnce.Do(func() { | |
| close(call.ch) | |
| }) | |
| } | |
| func (st *serverTester) Close() { | |
| if st.t.Failed() { | |
| st.frameReadLogMu.Lock() | |
| if st.frameReadLogBuf.Len() > 0 { | |
| st.t.Logf("Framer read log:\n%s", st.frameReadLogBuf.String()) | |
| } | |
| st.frameReadLogMu.Unlock() | |
| st.frameWriteLogMu.Lock() | |
| if st.frameWriteLogBuf.Len() > 0 { | |
| st.t.Logf("Framer write log:\n%s", st.frameWriteLogBuf.String()) | |
| } | |
| st.frameWriteLogMu.Unlock() | |
| // If we failed already (and are likely in a Fatal, | |
| // unwindowing), force close the connection, so the | |
| // httptest.Server doesn't wait forever for the conn | |
| // to close. | |
| if st.cc != nil { | |
| st.cc.Close() | |
| } | |
| } | |
| if st.cc != nil { | |
| st.cc.Close() | |
| } | |
| log.SetOutput(os.Stderr) | |
| } | |
| // greet initiates the client's HTTP/2 connection into a state where | |
| // frames may be sent. | |
| func (st *serverTester) greet() { | |
| st.t.Helper() | |
| st.greetAndCheckSettings(func(Setting) error { return nil }) | |
| } | |
| func (st *serverTester) greetAndCheckSettings(checkSetting func(s Setting) error) { | |
| st.t.Helper() | |
| st.writePreface() | |
| st.writeSettings() | |
| st.sync() | |
| readFrame[*SettingsFrame](st.t, st).ForeachSetting(checkSetting) | |
| st.writeSettingsAck() | |
| // The initial WINDOW_UPDATE and SETTINGS ACK can come in any order. | |
| var gotSettingsAck bool | |
| var gotWindowUpdate bool | |
| for i := 0; i < 2; i++ { | |
| f := st.readFrame() | |
| if f == nil { | |
| st.t.Fatal("wanted a settings ACK and window update, got none") | |
| } | |
| switch f := f.(type) { | |
| case *SettingsFrame: | |
| if !f.Header().Flags.Has(FlagSettingsAck) { | |
| st.t.Fatal("Settings Frame didn't have ACK set") | |
| } | |
| gotSettingsAck = true | |
| case *WindowUpdateFrame: | |
| if f.FrameHeader.StreamID != 0 { | |
| st.t.Fatalf("WindowUpdate StreamID = %d; want 0", f.FrameHeader.StreamID) | |
| } | |
| gotWindowUpdate = true | |
| default: | |
| st.t.Fatalf("Wanting a settings ACK or window update, received a %T", f) | |
| } | |
| } | |
| if !gotSettingsAck { | |
| st.t.Fatalf("Didn't get a settings ACK") | |
| } | |
| if !gotWindowUpdate { | |
| st.t.Fatalf("Didn't get a window update") | |
| } | |
| } | |
| func (st *serverTester) writePreface() { | |
| n, err := st.cc.Write([]byte(ClientPreface)) | |
| if err != nil { | |
| st.t.Fatalf("Error writing client preface: %v", err) | |
| } | |
| if n != len(ClientPreface) { | |
| st.t.Fatalf("Writing client preface, wrote %d bytes; want %d", n, len(ClientPreface)) | |
| } | |
| } | |
| func (st *serverTester) encodeHeaderField(k, v string) { | |
| err := st.hpackEnc.WriteField(hpack.HeaderField{Name: k, Value: v}) | |
| if err != nil { | |
| st.t.Fatalf("HPACK encoding error for %q/%q: %v", k, v, err) | |
| } | |
| } | |
| // encodeHeaderRaw is the magic-free version of encodeHeader. | |
| // It takes 0 or more (k, v) pairs and encodes them. | |
| func (st *serverTester) encodeHeaderRaw(headers ...string) []byte { | |
| if len(headers)%2 == 1 { | |
| panic("odd number of kv args") | |
| } | |
| st.headerBuf.Reset() | |
| for len(headers) > 0 { | |
| k, v := headers[0], headers[1] | |
| st.encodeHeaderField(k, v) | |
| headers = headers[2:] | |
| } | |
| return st.headerBuf.Bytes() | |
| } | |
| // encodeHeader encodes headers and returns their HPACK bytes. headers | |
| // must contain an even number of key/value pairs. There may be | |
| // multiple pairs for keys (e.g. "cookie"). The :method, :path, and | |
| // :scheme headers default to GET, / and https. The :authority header | |
| // defaults to st.ts.Listener.Addr(). | |
| func (st *serverTester) encodeHeader(headers ...string) []byte { | |
| if len(headers)%2 == 1 { | |
| panic("odd number of kv args") | |
| } | |
| st.headerBuf.Reset() | |
| defaultAuthority := st.authority() | |
| if len(headers) == 0 { | |
| // Fast path, mostly for benchmarks, so test code doesn't pollute | |
| // profiles when we're looking to improve server allocations. | |
| st.encodeHeaderField(":method", "GET") | |
| st.encodeHeaderField(":scheme", "https") | |
| st.encodeHeaderField(":authority", defaultAuthority) | |
| st.encodeHeaderField(":path", "/") | |
| return st.headerBuf.Bytes() | |
| } | |
| if len(headers) == 2 && headers[0] == ":method" { | |
| // Another fast path for benchmarks. | |
| st.encodeHeaderField(":method", headers[1]) | |
| st.encodeHeaderField(":scheme", "https") | |
| st.encodeHeaderField(":authority", defaultAuthority) | |
| st.encodeHeaderField(":path", "/") | |
| return st.headerBuf.Bytes() | |
| } | |
| pseudoCount := map[string]int{} | |
| keys := []string{":method", ":scheme", ":authority", ":path"} | |
| vals := map[string][]string{ | |
| ":method": {"GET"}, | |
| ":scheme": {"https"}, | |
| ":authority": {defaultAuthority}, | |
| ":path": {"/"}, | |
| } | |
| for len(headers) > 0 { | |
| k, v := headers[0], headers[1] | |
| headers = headers[2:] | |
| if _, ok := vals[k]; !ok { | |
| keys = append(keys, k) | |
| } | |
| if strings.HasPrefix(k, ":") { | |
| pseudoCount[k]++ | |
| if pseudoCount[k] == 1 { | |
| vals[k] = []string{v} | |
| } else { | |
| // Allows testing of invalid headers w/ dup pseudo fields. | |
| vals[k] = append(vals[k], v) | |
| } | |
| } else { | |
| vals[k] = append(vals[k], v) | |
| } | |
| } | |
| for _, k := range keys { | |
| for _, v := range vals[k] { | |
| st.encodeHeaderField(k, v) | |
| } | |
| } | |
| return st.headerBuf.Bytes() | |
| } | |
| // bodylessReq1 writes a HEADERS frames with StreamID 1 and EndStream and EndHeaders set. | |
| func (st *serverTester) bodylessReq1(headers ...string) { | |
| st.writeHeaders(HeadersFrameParam{ | |
| StreamID: 1, // clients send odd numbers | |
| BlockFragment: st.encodeHeader(headers...), | |
| EndStream: true, | |
| EndHeaders: true, | |
| }) | |
| } | |
| // sync waits for all goroutines to idle. | |
| func (st *serverTester) sync() { | |
| synctest.Wait() | |
| } | |
| // advance advances synthetic time by a duration. | |
| func (st *serverTester) advance(d time.Duration) { | |
| time.Sleep(d) | |
| synctest.Wait() | |
| } | |
| func (st *serverTester) authority() string { | |
| return "dummy.tld" | |
| } | |
| func (st *serverTester) addLogFilter(phrase string) { | |
| st.logFilter = append(st.logFilter, phrase) | |
| } | |
| func (st *serverTester) nextHandlerCall() *serverHandlerCall { | |
| st.t.Helper() | |
| synctest.Wait() | |
| st.callsMu.Lock() | |
| defer st.callsMu.Unlock() | |
| if len(st.calls) == 0 { | |
| st.t.Fatal("expected server handler call, got none") | |
| } | |
| call := st.calls[0] | |
| st.calls = st.calls[1:] | |
| return call | |
| } |