| // Copyright 2015 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. | |
| // Tests that use both the client & server, in both HTTP/1 and HTTP/2 mode. | |
| package http_test | |
| import ( | |
| "bytes" | |
| "compress/gzip" | |
| "context" | |
| "crypto/rand" | |
| "crypto/sha1" | |
| "crypto/tls" | |
| "fmt" | |
| "hash" | |
| "internal/nettest" | |
| "io" | |
| "log" | |
| "maps" | |
| "net" | |
| . "net/http" | |
| "net/http/httptest" | |
| "net/http/httptrace" | |
| "net/http/httputil" | |
| "net/netip" | |
| "net/textproto" | |
| "net/url" | |
| "os" | |
| "reflect" | |
| "runtime" | |
| "slices" | |
| "strings" | |
| "sync" | |
| "sync/atomic" | |
| "testing" | |
| "testing/synctest" | |
| "time" | |
| "net/http/internal/http3" | |
| ) | |
| type testMode string | |
| const ( | |
| http1Mode = testMode("h1") // HTTP/1.1 | |
| https1Mode = testMode("https1") // HTTPS/1.1 | |
| http2Mode = testMode("h2") // HTTP/2 | |
| http2UnencryptedMode = testMode("h2unencrypted") // HTTP/2 | |
| http3Mode = testMode("h3") // HTTP/3 | |
| ) | |
| type ( | |
| testAddMode []testMode // default, plus these | |
| testSkipMode []testMode // default, minus these | |
| ) | |
| // http3SkippedMode is a convenient alias for []testMode{http1Mode, http2Mode}, | |
| // which was the default test mode used by run and runSynctest prior to HTTP/3 | |
| // development. | |
| // As we work on getting net/http tests to pass for our x/net HTTP/3 | |
| // implementation, tests that still use http3SkippedMode are essentially a list | |
| // of TODOs on what work needs to be done for our HTTP/3 implementation to | |
| // reach basic feature parity with our HTTP/1 and HTTP/2 implementations | |
| var http3SkippedMode = []testMode{http1Mode, http2Mode} | |
| func (m testMode) Scheme() string { | |
| switch m { | |
| case http1Mode, http2UnencryptedMode: | |
| return "http" | |
| case https1Mode, http2Mode, http3Mode: | |
| return "https" | |
| } | |
| panic("unknown testMode") | |
| } | |
| type ( | |
| testNotParallelOpt struct{} | |
| testRealNetOpt struct{} | |
| ) | |
| var ( | |
| testNotParallel = testNotParallelOpt{} | |
| optRealNet = testRealNetOpt{} | |
| ) | |
| type TBRun[T any] interface { | |
| testing.TB | |
| Run(string, func(T)) bool | |
| } | |
| // run runs a client/server test in a variety of test configurations. | |
| // | |
| // Tests execute in HTTP/1.1 and HTTP/2 modes by default. | |
| // To run in a different set of configurations, pass a []testMode option. | |
| // | |
| // Tests call t.Parallel() by default. | |
| // To disable parallel execution, pass the testNotParallel option. | |
| func run[T TBRun[T]](t T, f func(t T, mode testMode), opts ...any) { | |
| t.Helper() | |
| modes := []testMode{http1Mode, http2Mode, http3Mode} | |
| parallel := true | |
| for _, opt := range opts { | |
| switch opt := opt.(type) { | |
| case testAddMode: | |
| for _, m := range opt { | |
| if !slices.Contains(modes, m) { | |
| modes = append(modes, m) | |
| } | |
| } | |
| case testSkipMode: | |
| modes = slices.DeleteFunc(modes, func(m testMode) bool { | |
| return slices.Contains(opt, m) | |
| }) | |
| case []testMode: | |
| modes = opt | |
| case testNotParallelOpt: | |
| parallel = false | |
| default: | |
| t.Fatalf("unknown option type %T", opt) | |
| } | |
| } | |
| if t, ok := any(t).(*testing.T); ok && parallel { | |
| setParallel(t) | |
| } | |
| for _, mode := range modes { | |
| t.Run(string(mode), func(t T) { | |
| panicking := false | |
| defer func() { | |
| if err := recover(); err != nil { | |
| panicking = true | |
| panic(err) | |
| } | |
| }() | |
| t.Helper() | |
| if t, ok := any(t).(*testing.T); ok && parallel { | |
| setParallel(t) | |
| } | |
| t.Cleanup(func() { | |
| if !panicking { | |
| afterTest(t) | |
| } | |
| }) | |
| f(t, mode) | |
| }) | |
| } | |
| } | |
| // runSynctest is run combined with synctest.Run. | |
| // | |
| // The TB passed to f arranges for cleanup functions to be run in the synctest bubble. | |
| func runSynctest(t *testing.T, f func(t *testing.T, mode testMode), opts ...any) { | |
| run(t, func(t *testing.T, mode testMode) { | |
| synctest.Test(t, func(t *testing.T) { | |
| f(t, mode) | |
| }) | |
| }, opts...) | |
| } | |
| type clientServerTest struct { | |
| t testing.TB | |
| h2 bool | |
| h Handler | |
| ts *httptest.Server | |
| tr *Transport | |
| c *Client | |
| mode testMode | |
| } | |
| func (t *clientServerTest) close() { | |
| t.tr.CloseIdleConnections() | |
| t.ts.Close() | |
| } | |
| func (t *clientServerTest) getURL(u string) string { | |
| res, err := t.c.Get(u) | |
| if err != nil { | |
| t.t.Fatal(err) | |
| } | |
| defer res.Body.Close() | |
| slurp, err := io.ReadAll(res.Body) | |
| if err != nil { | |
| t.t.Fatal(err) | |
| } | |
| return string(slurp) | |
| } | |
| func (t *clientServerTest) scheme() string { | |
| if t.h2 { | |
| return "https" | |
| } | |
| return "http" | |
| } | |
| var optQuietLog = func(ts *httptest.Server) { | |
| ts.Config.ErrorLog = quietLog | |
| } | |
| func optWithServerLog(lg *log.Logger) func(*httptest.Server) { | |
| return func(ts *httptest.Server) { | |
| ts.Config.ErrorLog = lg | |
| } | |
| } | |
| // newClientServerTest creates and starts an httptest.Server. | |
| // | |
| // The mode parameter selects the implementation to test: | |
| // HTTP/1, HTTP/2, etc. Tests using newClientServerTest should use | |
| // the 'run' function, which will start a subtests for each tested mode. | |
| // | |
| // The vararg opts parameter can include functions to configure the | |
| // test server or transport. | |
| // | |
| // func(*httptest.Server) // run before starting the server | |
| // func(*http.Transport) | |
| // | |
| // The optRealNet option configures the server and client to use a loopback network connection. | |
| // This is mostly used for tests which predate fake networking support, | |
| // which haven't been updated yet. | |
| func newClientServerTest(t testing.TB, mode testMode, h Handler, opts ...any) *clientServerTest { | |
| if mode == http2Mode || mode == http2UnencryptedMode { | |
| CondSkipHTTP2(t) | |
| } | |
| cst := &clientServerTest{ | |
| t: t, | |
| h2: mode == http2Mode, | |
| h: h, | |
| mode: mode, | |
| } | |
| var transportFuncs []func(*Transport) | |
| var http3TransportOpts http3.TransportOpts | |
| fakeNet := true | |
| if idx := slices.Index(opts, any(optRealNet)); idx >= 0 { | |
| fakeNet = false | |
| } | |
| switch { | |
| case fakeNet && mode != http3Mode: | |
| cst.ts = httptest.NewTestServer(t, h) | |
| case mode == http3Mode: | |
| cst.ts = &httptest.Server{ | |
| Config: &Server{Handler: h}, | |
| } | |
| if fakeNet { | |
| pnet := nettest.NewPacketNet() | |
| srvAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:443")) | |
| var cliPort atomic.Uint32 | |
| cliPort.Store(100) | |
| pconn, _ := pnet.NewConn(srvAddr) | |
| cst.ts.Listener = http3ServeConn{conn: pconn} | |
| http3TransportOpts.ListenPacket = func(network, addr string) (net.PacketConn, error) { | |
| port := uint16(cliPort.Add(1)) | |
| cliAddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom( | |
| netip.MustParseAddr("127.0.0.1"), port)) | |
| return pnet.NewConn(cliAddr) | |
| } | |
| } else { | |
| pconn := newLocalPacketConn(t) | |
| cst.ts.Listener = http3ServeConn{conn: pconn} | |
| // Bind the client to the same address as the server, rather than | |
| // letting it use the default of the ":0" wildcard address. | |
| // | |
| // On Darwin, a wildcard bind creates a dual-stack IPv6 socket, and | |
| // closing a UDP socket does not immediately free its port. A client | |
| // which is coincidentally assigned a port that is still in use on | |
| // IPv4 but free on IPv6 still binds successfully, but silently | |
| // cannot receive packets sent to its IPv4 address. Since the server | |
| // here listens on a loopback address, that leaves the client unable | |
| // to receive the server's handshake packets. | |
| // See go.dev/issue/78737 and go.dev/issue/81162. | |
| host, _, err := net.SplitHostPort(pconn.LocalAddr().String()) | |
| if err != nil { | |
| t.Fatalf("parsing server address %v: %v", pconn.LocalAddr(), err) | |
| } | |
| http3TransportOpts.ListenPacket = func(network, addr string) (net.PacketConn, error) { | |
| return net.ListenPacket(network, net.JoinHostPort(host, "0")) | |
| } | |
| } | |
| default: | |
| cst.ts = httptest.NewUnstartedServer(h) | |
| } | |
| for _, opt := range opts { | |
| switch opt := opt.(type) { | |
| case func(*Transport): | |
| transportFuncs = append(transportFuncs, opt) | |
| case func(*httptest.Server): | |
| opt(cst.ts) | |
| case func(*Server): | |
| opt(cst.ts.Config) | |
| case testRealNetOpt: | |
| default: | |
| t.Fatalf("unhandled option type %T", opt) | |
| } | |
| } | |
| if cst.ts.Config.ErrorLog == nil { | |
| cst.ts.Config.ErrorLog = log.New(testLogWriter{t}, "", 0) | |
| } | |
| p := &Protocols{} | |
| if cst.ts.Config.Protocols == nil { | |
| cst.ts.Config.Protocols = p | |
| } | |
| switch mode { | |
| case http1Mode: | |
| p.SetHTTP1(true) | |
| case https1Mode: | |
| p.SetHTTP1(true) | |
| case http2UnencryptedMode: | |
| p.SetUnencryptedHTTP2(true) | |
| case http2Mode: | |
| p.SetHTTP2(true) | |
| cst.ts.EnableHTTP2 = true | |
| cst.ts.TLS = cst.ts.Config.TLSConfig | |
| case http3Mode: | |
| p.SetHTTP3(true) | |
| cst.ts.TLS = cst.ts.Config.TLSConfig | |
| if err := http3.RegisterServer(cst.ts.Config, http3.ServerOpts{}); err != nil { | |
| t.Fatal(err) | |
| } | |
| cst.ts.StartTLS() | |
| tr := cst.ts.Client().Transport.(*Transport) | |
| if err := http3.RegisterTransport(tr, http3TransportOpts); err != nil { | |
| t.Fatal(err) | |
| } | |
| default: | |
| t.Fatalf("unknown test mode %v", mode) | |
| } | |
| if !fakeNet { | |
| switch mode { | |
| case http1Mode, http2UnencryptedMode: | |
| cst.ts.Start() | |
| case https1Mode, http2Mode: | |
| cst.ts.StartTLS() | |
| } | |
| } | |
| cst.c = cst.ts.Client() // sets cst.ts.URL if it wasn't already | |
| // Fakenet httptest servers always set the URL scheme to "http". | |
| // Override it in https tests. | |
| // Exclude HTTP3, which manages its own PacketNet URL, and lacks the | |
| // ability to intercept "example.com". | |
| if fakeNet && mode != http3Mode { | |
| cst.ts.URL = mode.Scheme() + "://example.com" | |
| } | |
| cst.tr = cst.c.Transport.(*Transport) | |
| for _, f := range transportFuncs { | |
| f(cst.tr) | |
| } | |
| if cst.tr.Protocols == nil { | |
| cst.tr.Protocols = p | |
| } | |
| t.Cleanup(func() { | |
| cst.close() | |
| }) | |
| return cst | |
| } | |
| func newLocalPacketConn(t testing.TB) net.PacketConn { | |
| t.Helper() | |
| pc, err := net.ListenPacket("udp", "127.0.0.1:0") | |
| if err != nil { | |
| if pc, err = net.ListenPacket("udp6", "[::1]:0"); err != nil { | |
| t.Fatalf("failed to listen on a port: %v", err) | |
| } | |
| } | |
| return pc | |
| } | |
| // http3ServeConn provides a HTTP/3 PacketConn to Server.ServeTLS. | |
| type http3ServeConn struct { | |
| conn net.PacketConn | |
| } | |
| func (c http3ServeConn) Accept() (net.Conn, error) { return nil, net.ErrClosed } | |
| func (c http3ServeConn) Close() error { return c.conn.Close() } | |
| func (c http3ServeConn) Addr() net.Addr { return c.conn.LocalAddr() } | |
| func (c http3ServeConn) HTTP3PacketConn() net.PacketConn { | |
| return c.conn | |
| } | |
| func (cst *clientServerTest) dialNettest() (net.Conn, *nettest.Conn) { | |
| cst.t.Helper() | |
| dial := cst.tr.DialContext | |
| if cst.mode.Scheme() == "https" { | |
| dial = cst.tr.DialTLSContext | |
| } | |
| if dial == nil { | |
| cst.t.Fatalf("tr.Dial(TLS) is not set") | |
| } | |
| conn, err := dial(cst.t.Context(), "tcp", "example.tld:"+cst.mode.Scheme()) | |
| if err != nil { | |
| cst.t.Fatal(err) | |
| } | |
| cst.t.Cleanup(func() { | |
| conn.Close() | |
| }) | |
| if tlsConn, ok := conn.(*tls.Conn); ok { | |
| return tlsConn, tlsConn.NetConn().(*nettest.Conn) | |
| } | |
| return conn, conn.(*nettest.Conn) | |
| } | |
| func (cst *clientServerTest) setDialNettestHook(f func(*nettest.Conn)) { | |
| dialContext := cst.tr.DialContext | |
| cst.tr.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { | |
| conn, err := dialContext(ctx, network, address) | |
| if err == nil { | |
| f(conn.(*nettest.Conn)) | |
| } | |
| return conn, err | |
| } | |
| dialTLSContext := cst.tr.DialTLSContext | |
| cst.tr.DialTLSContext = func(ctx context.Context, network, address string) (net.Conn, error) { | |
| conn, err := dialTLSContext(ctx, network, address) | |
| if err == nil { | |
| f(conn.(*tls.Conn).NetConn().(*nettest.Conn)) | |
| } | |
| return conn, err | |
| } | |
| } | |
| type testLogWriter struct { | |
| t testing.TB | |
| } | |
| func (w testLogWriter) Write(b []byte) (int, error) { | |
| w.t.Logf("server log: %v", strings.TrimSpace(string(b))) | |
| return len(b), nil | |
| } | |
| // Testing the newClientServerTest helper itself. | |
| func TestNewClientServerTest(t *testing.T) { | |
| modes := []testMode{http1Mode, https1Mode, http2Mode} | |
| t.Run("realnet", func(t *testing.T) { | |
| run(t, func(t *testing.T, mode testMode) { | |
| testNewClientServerTest(t, mode) | |
| }, modes) | |
| }) | |
| t.Run("synctest", func(t *testing.T) { | |
| runSynctest(t, func(t *testing.T, mode testMode) { | |
| testNewClientServerTest(t, mode) | |
| }, modes) | |
| }) | |
| } | |
| func testNewClientServerTest(t *testing.T, mode testMode, opts ...any) { | |
| var got struct { | |
| sync.Mutex | |
| proto string | |
| hasTLS bool | |
| } | |
| h := HandlerFunc(func(w ResponseWriter, r *Request) { | |
| got.Lock() | |
| defer got.Unlock() | |
| got.proto = r.Proto | |
| got.hasTLS = r.TLS != nil | |
| }) | |
| cst := newClientServerTest(t, mode, h, opts...) | |
| if _, err := cst.c.Head(mode.Scheme() + "://example.tld/"); err != nil { | |
| t.Fatal(err) | |
| } | |
| var wantProto string | |
| var wantTLS bool | |
| switch mode { | |
| case http1Mode: | |
| wantProto = "HTTP/1.1" | |
| wantTLS = false | |
| case https1Mode: | |
| wantProto = "HTTP/1.1" | |
| wantTLS = true | |
| case http2Mode: | |
| wantProto = "HTTP/2.0" | |
| wantTLS = true | |
| } | |
| if got.proto != wantProto { | |
| t.Errorf("req.Proto = %q, want %q", got.proto, wantProto) | |
| } | |
| if got.hasTLS != wantTLS { | |
| t.Errorf("req.TLS set: %v, want %v", got.hasTLS, wantTLS) | |
| } | |
| } | |
| func TestChunkedResponseHeaders(t *testing.T) { | |
| run(t, testChunkedResponseHeaders) | |
| } | |
| func testChunkedResponseHeaders(t *testing.T, mode testMode) { | |
| log.SetOutput(io.Discard) // is noisy otherwise | |
| defer log.SetOutput(os.Stderr) | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| w.Header().Set("Content-Length", "intentional gibberish") // we check that this is deleted | |
| w.(Flusher).Flush() | |
| fmt.Fprintf(w, "I am a chunked response.") | |
| })) | |
| res, err := cst.c.Get(cst.ts.URL) | |
| if err != nil { | |
| t.Fatalf("Get error: %v", err) | |
| } | |
| defer res.Body.Close() | |
| if g, e := res.ContentLength, int64(-1); g != e { | |
| t.Errorf("expected ContentLength of %d; got %d", e, g) | |
| } | |
| wantTE := []string{"chunked"} | |
| if mode == http2Mode || mode == http3Mode { | |
| wantTE = nil | |
| } | |
| if !slices.Equal(res.TransferEncoding, wantTE) { | |
| t.Errorf("TransferEncoding = %v; want %v", res.TransferEncoding, wantTE) | |
| } | |
| if got, haveCL := res.Header["Content-Length"]; haveCL { | |
| t.Errorf("Unexpected Content-Length: %q", got) | |
| } | |
| } | |
| type reqFunc func(c *Client, url string) (*Response, error) | |
| // h12Compare is a test that compares HTTP/1 and HTTP/2 behavior | |
| // against each other. | |
| type h12Compare struct { | |
| Handler func(ResponseWriter, *Request) // required | |
| ReqFunc reqFunc // optional | |
| CheckResponse func(proto string, res *Response) // optional | |
| EarlyCheckResponse func(proto string, res *Response) // optional; pre-normalize | |
| Opts []any | |
| } | |
| func (tt h12Compare) reqFunc() reqFunc { | |
| if tt.ReqFunc == nil { | |
| return (*Client).Get | |
| } | |
| return tt.ReqFunc | |
| } | |
| func (tt h12Compare) run(t *testing.T) { | |
| setParallel(t) | |
| // Using a real network because httptest's fake networking mode doesn't (yet) | |
| // support multiple servers on the same fake net. | |
| opts := append(tt.Opts, optRealNet) | |
| cst1 := newClientServerTest(t, http1Mode, HandlerFunc(tt.Handler), opts...) | |
| defer cst1.close() | |
| cst2 := newClientServerTest(t, http2Mode, HandlerFunc(tt.Handler), opts...) | |
| defer cst2.close() | |
| res1, err := tt.reqFunc()(cst1.c, cst1.ts.URL) | |
| if err != nil { | |
| t.Errorf("HTTP/1 request: %v", err) | |
| return | |
| } | |
| res2, err := tt.reqFunc()(cst2.c, cst2.ts.URL) | |
| if err != nil { | |
| t.Errorf("HTTP/2 request: %v", err) | |
| return | |
| } | |
| if fn := tt.EarlyCheckResponse; fn != nil { | |
| fn("HTTP/1.1", res1) | |
| fn("HTTP/2.0", res2) | |
| } | |
| tt.normalizeRes(t, res1, "HTTP/1.1") | |
| tt.normalizeRes(t, res2, "HTTP/2.0") | |
| res1body, res2body := res1.Body, res2.Body | |
| eres1 := mostlyCopy(res1) | |
| eres2 := mostlyCopy(res2) | |
| if !reflect.DeepEqual(eres1, eres2) { | |
| t.Errorf("Response headers to handler differed:\nhttp/1 (%v):\n\t%#v\nhttp/2 (%v):\n\t%#v", | |
| cst1.ts.URL, eres1, cst2.ts.URL, eres2) | |
| } | |
| if !reflect.DeepEqual(res1body, res2body) { | |
| t.Errorf("Response bodies to handler differed.\nhttp1: %v\nhttp2: %v\n", res1body, res2body) | |
| } | |
| if fn := tt.CheckResponse; fn != nil { | |
| res1.Body, res2.Body = res1body, res2body | |
| fn("HTTP/1.1", res1) | |
| fn("HTTP/2.0", res2) | |
| } | |
| } | |
| func mostlyCopy(r *Response) *Response { | |
| c := *r | |
| c.Body = nil | |
| c.TransferEncoding = nil | |
| c.TLS = nil | |
| c.Request = nil | |
| return &c | |
| } | |
| type slurpResult struct { | |
| io.ReadCloser | |
| body []byte | |
| err error | |
| } | |
| func (sr slurpResult) String() string { return fmt.Sprintf("body %q; err %v", sr.body, sr.err) } | |
| func (tt h12Compare) normalizeRes(t *testing.T, res *Response, wantProto string) { | |
| if res.Proto == wantProto || res.Proto == "HTTP/IGNORE" { | |
| res.Proto, res.ProtoMajor, res.ProtoMinor = "", 0, 0 | |
| } else { | |
| t.Errorf("got %q response; want %q", res.Proto, wantProto) | |
| } | |
| slurp, err := io.ReadAll(res.Body) | |
| res.Body.Close() | |
| res.Body = slurpResult{ | |
| ReadCloser: io.NopCloser(bytes.NewReader(slurp)), | |
| body: slurp, | |
| err: err, | |
| } | |
| for i, v := range res.Header["Date"] { | |
| res.Header["Date"][i] = strings.Repeat("x", len(v)) | |
| } | |
| if res.Request == nil { | |
| t.Errorf("for %s, no request", wantProto) | |
| } | |
| if (res.TLS != nil) != (wantProto == "HTTP/2.0") { | |
| t.Errorf("TLS set = %v; want %v", res.TLS != nil, res.TLS == nil) | |
| } | |
| } | |
| // Issue 13532 | |
| func TestH12_HeadContentLengthNoBody(t *testing.T) { | |
| h12Compare{ | |
| ReqFunc: (*Client).Head, | |
| Handler: func(w ResponseWriter, r *Request) { | |
| }, | |
| }.run(t) | |
| } | |
| func TestH12_HeadContentLengthSmallBody(t *testing.T) { | |
| h12Compare{ | |
| ReqFunc: (*Client).Head, | |
| Handler: func(w ResponseWriter, r *Request) { | |
| io.WriteString(w, "small") | |
| }, | |
| }.run(t) | |
| } | |
| func TestH12_HeadContentLengthLargeBody(t *testing.T) { | |
| h12Compare{ | |
| ReqFunc: (*Client).Head, | |
| Handler: func(w ResponseWriter, r *Request) { | |
| chunk := strings.Repeat("x", 512<<10) | |
| for i := 0; i < 10; i++ { | |
| io.WriteString(w, chunk) | |
| } | |
| }, | |
| }.run(t) | |
| } | |
| func TestH12_200NoBody(t *testing.T) { | |
| h12Compare{Handler: func(w ResponseWriter, r *Request) {}}.run(t) | |
| } | |
| func TestH2_204NoBody(t *testing.T) { testH12_noBody(t, 204) } | |
| func TestH2_304NoBody(t *testing.T) { testH12_noBody(t, 304) } | |
| func TestH2_404NoBody(t *testing.T) { testH12_noBody(t, 404) } | |
| func testH12_noBody(t *testing.T, status int) { | |
| h12Compare{Handler: func(w ResponseWriter, r *Request) { | |
| w.WriteHeader(status) | |
| }}.run(t) | |
| } | |
| func TestH12_SmallBody(t *testing.T) { | |
| h12Compare{Handler: func(w ResponseWriter, r *Request) { | |
| io.WriteString(w, "small body") | |
| }}.run(t) | |
| } | |
| func TestH12_ExplicitContentLength(t *testing.T) { | |
| h12Compare{Handler: func(w ResponseWriter, r *Request) { | |
| w.Header().Set("Content-Length", "3") | |
| io.WriteString(w, "foo") | |
| }}.run(t) | |
| } | |
| func TestH12_FlushBeforeBody(t *testing.T) { | |
| h12Compare{Handler: func(w ResponseWriter, r *Request) { | |
| w.(Flusher).Flush() | |
| io.WriteString(w, "foo") | |
| }}.run(t) | |
| } | |
| func TestH12_FlushMidBody(t *testing.T) { | |
| h12Compare{Handler: func(w ResponseWriter, r *Request) { | |
| io.WriteString(w, "foo") | |
| w.(Flusher).Flush() | |
| io.WriteString(w, "bar") | |
| }}.run(t) | |
| } | |
| func TestH12_Head_ExplicitLen(t *testing.T) { | |
| h12Compare{ | |
| ReqFunc: (*Client).Head, | |
| Handler: func(w ResponseWriter, r *Request) { | |
| if r.Method != "HEAD" { | |
| t.Errorf("unexpected method %q", r.Method) | |
| } | |
| w.Header().Set("Content-Length", "1235") | |
| }, | |
| }.run(t) | |
| } | |
| func TestH12_Head_ImplicitLen(t *testing.T) { | |
| h12Compare{ | |
| ReqFunc: (*Client).Head, | |
| Handler: func(w ResponseWriter, r *Request) { | |
| if r.Method != "HEAD" { | |
| t.Errorf("unexpected method %q", r.Method) | |
| } | |
| io.WriteString(w, "foo") | |
| }, | |
| }.run(t) | |
| } | |
| func TestH12_HandlerWritesTooLittle(t *testing.T) { | |
| h12Compare{ | |
| Handler: func(w ResponseWriter, r *Request) { | |
| w.Header().Set("Content-Length", "3") | |
| io.WriteString(w, "12") // one byte short | |
| }, | |
| CheckResponse: func(proto string, res *Response) { | |
| sr, ok := res.Body.(slurpResult) | |
| if !ok { | |
| t.Errorf("%s body is %T; want slurpResult", proto, res.Body) | |
| return | |
| } | |
| if sr.err != io.ErrUnexpectedEOF { | |
| t.Errorf("%s read error = %v; want io.ErrUnexpectedEOF", proto, sr.err) | |
| } | |
| if string(sr.body) != "12" { | |
| t.Errorf("%s body = %q; want %q", proto, sr.body, "12") | |
| } | |
| }, | |
| }.run(t) | |
| } | |
| // Tests that the HTTP/1 and HTTP/2 servers prevent handlers from | |
| // writing more than they declared. This test does not test whether | |
| // the transport deals with too much data, though, since the server | |
| // doesn't make it possible to send bogus data. For those tests, see | |
| // transport_test.go (for HTTP/1) or x/net/http2/transport_test.go | |
| // (for HTTP/2). | |
| func TestHandlerWritesTooMuch(t *testing.T) { run(t, testHandlerWritesTooMuch) } | |
| func testHandlerWritesTooMuch(t *testing.T, mode testMode) { | |
| wantBody := []byte("123") | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| rc := NewResponseController(w) | |
| w.Header().Set("Content-Length", fmt.Sprintf("%v", len(wantBody))) | |
| rc.Flush() | |
| w.Write(wantBody) | |
| rc.Flush() | |
| n, err := io.WriteString(w, "x") // too many | |
| if err == nil { | |
| err = rc.Flush() | |
| } | |
| // TODO: Check that this is ErrContentLength, not just any error. | |
| if err == nil { | |
| t.Errorf("for proto %q, final write = %v, %v; want _, some error", r.Proto, n, err) | |
| } | |
| })) | |
| res, err := cst.c.Get(cst.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer res.Body.Close() | |
| gotBody, _ := io.ReadAll(res.Body) | |
| if !bytes.Equal(gotBody, wantBody) { | |
| t.Fatalf("got response body: %q; want %q", gotBody, wantBody) | |
| } | |
| } | |
| // Verify that both our HTTP/1 and HTTP/2 request and auto-decompress gzip. | |
| // Some hosts send gzip even if you don't ask for it; see golang.org/issue/13298 | |
| func TestH12_AutoGzip(t *testing.T) { | |
| h12Compare{ | |
| Handler: func(w ResponseWriter, r *Request) { | |
| if ae := r.Header.Get("Accept-Encoding"); ae != "gzip" { | |
| t.Errorf("%s Accept-Encoding = %q; want gzip", r.Proto, ae) | |
| } | |
| w.Header().Set("Content-Encoding", "gzip") | |
| gz := gzip.NewWriter(w) | |
| io.WriteString(gz, "I am some gzipped content. Go go go go go go go go go go go go should compress well.") | |
| gz.Close() | |
| }, | |
| }.run(t) | |
| } | |
| func TestH12_AutoGzip_Disabled(t *testing.T) { | |
| h12Compare{ | |
| Opts: []any{ | |
| func(tr *Transport) { tr.DisableCompression = true }, | |
| }, | |
| Handler: func(w ResponseWriter, r *Request) { | |
| fmt.Fprintf(w, "%q", r.Header["Accept-Encoding"]) | |
| if ae := r.Header.Get("Accept-Encoding"); ae != "" { | |
| t.Errorf("%s Accept-Encoding = %q; want empty", r.Proto, ae) | |
| } | |
| }, | |
| }.run(t) | |
| } | |
| // Test304Responses verifies that 304s don't declare that they're | |
| // chunking in their response headers and aren't allowed to produce | |
| // output. | |
| func Test304Responses(t *testing.T) { run(t, test304Responses) } | |
| func test304Responses(t *testing.T, mode testMode) { | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| w.WriteHeader(StatusNotModified) | |
| _, err := w.Write([]byte("illegal body")) | |
| if err != ErrBodyNotAllowed { | |
| t.Errorf("on Write, expected ErrBodyNotAllowed, got %v", err) | |
| } | |
| })) | |
| defer cst.close() | |
| res, err := cst.c.Get(cst.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(res.TransferEncoding) > 0 { | |
| t.Errorf("expected no TransferEncoding; got %v", res.TransferEncoding) | |
| } | |
| body, err := io.ReadAll(res.Body) | |
| if err != nil { | |
| t.Error(err) | |
| } | |
| if len(body) > 0 { | |
| t.Errorf("got unexpected body %q", string(body)) | |
| } | |
| } | |
| func TestH12_ServerEmptyContentLength(t *testing.T) { | |
| h12Compare{ | |
| Handler: func(w ResponseWriter, r *Request) { | |
| w.Header()["Content-Type"] = []string{""} | |
| io.WriteString(w, "<html><body>hi</body></html>") | |
| }, | |
| }.run(t) | |
| } | |
| func TestH12_RequestContentLength_Known_NonZero(t *testing.T) { | |
| h12requestContentLength(t, func() io.Reader { return strings.NewReader("FOUR") }, 4) | |
| } | |
| func TestH12_RequestContentLength_Known_Zero(t *testing.T) { | |
| h12requestContentLength(t, func() io.Reader { return nil }, 0) | |
| } | |
| func TestH12_RequestContentLength_Unknown(t *testing.T) { | |
| h12requestContentLength(t, func() io.Reader { return struct{ io.Reader }{strings.NewReader("Stuff")} }, -1) | |
| } | |
| func h12requestContentLength(t *testing.T, bodyfn func() io.Reader, wantLen int64) { | |
| h12Compare{ | |
| Handler: func(w ResponseWriter, r *Request) { | |
| w.Header().Set("Got-Length", fmt.Sprint(r.ContentLength)) | |
| fmt.Fprintf(w, "Req.ContentLength=%v", r.ContentLength) | |
| }, | |
| ReqFunc: func(c *Client, url string) (*Response, error) { | |
| return c.Post(url, "text/plain", bodyfn()) | |
| }, | |
| CheckResponse: func(proto string, res *Response) { | |
| if got, want := res.Header.Get("Got-Length"), fmt.Sprint(wantLen); got != want { | |
| t.Errorf("Proto %q got length %q; want %q", proto, got, want) | |
| } | |
| }, | |
| }.run(t) | |
| } | |
| // Tests that closing the Request.Cancel channel also while still | |
| // reading the response body. Issue 13159. | |
| func TestCancelRequestMidBody(t *testing.T) { run(t, testCancelRequestMidBody, http3SkippedMode) } | |
| func testCancelRequestMidBody(t *testing.T, mode testMode) { | |
| unblock := make(chan bool) | |
| didFlush := make(chan bool, 1) | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| io.WriteString(w, "Hello") | |
| w.(Flusher).Flush() | |
| didFlush <- true | |
| <-unblock | |
| io.WriteString(w, ", world.") | |
| })) | |
| defer close(unblock) | |
| req, _ := NewRequest("GET", cst.ts.URL, nil) | |
| cancel := make(chan struct{}) | |
| req.Cancel = cancel | |
| res, err := cst.c.Do(req) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer res.Body.Close() | |
| <-didFlush | |
| // Read a bit before we cancel. (Issue 13626) | |
| // We should have "Hello" at least sitting there. | |
| firstRead := make([]byte, 10) | |
| n, err := res.Body.Read(firstRead) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| firstRead = firstRead[:n] | |
| close(cancel) | |
| rest, err := io.ReadAll(res.Body) | |
| all := string(firstRead) + string(rest) | |
| if all != "Hello" { | |
| t.Errorf("Read %q (%q + %q); want Hello", all, firstRead, rest) | |
| } | |
| if err != ExportErrRequestCanceled { | |
| t.Errorf("ReadAll error = %v; want %v", err, ExportErrRequestCanceled) | |
| } | |
| } | |
| // Tests that clients can send trailers to a server and that the server can read them. | |
| func TestTrailersClientToServer(t *testing.T) { run(t, testTrailersClientToServer) } | |
| func testTrailersClientToServer(t *testing.T, mode testMode) { | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| slurp, err := io.ReadAll(r.Body) | |
| if err != nil { | |
| t.Errorf("Server reading request body: %v", err) | |
| } | |
| if string(slurp) != "foo" { | |
| t.Errorf("Server read request body %q; want foo", slurp) | |
| } | |
| if r.Trailer == nil { | |
| io.WriteString(w, "nil Trailer") | |
| } else { | |
| decl := slices.Sorted(maps.Keys(r.Trailer)) | |
| fmt.Fprintf(w, "decl: %v, vals: %s, %s", | |
| decl, | |
| r.Trailer.Get("Client-Trailer-A"), | |
| r.Trailer.Get("Client-Trailer-B")) | |
| } | |
| })) | |
| var req *Request | |
| req, _ = NewRequest("POST", cst.ts.URL, io.MultiReader( | |
| eofReaderFunc(func() { | |
| req.Trailer["Client-Trailer-A"] = []string{"valuea"} | |
| }), | |
| strings.NewReader("foo"), | |
| eofReaderFunc(func() { | |
| req.Trailer["Client-Trailer-B"] = []string{"valueb"} | |
| }), | |
| )) | |
| req.Trailer = Header{ | |
| "Client-Trailer-A": nil, // to be set later | |
| "Client-Trailer-B": nil, // to be set later | |
| } | |
| req.ContentLength = -1 | |
| res, err := cst.c.Do(req) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := wantBody(res, err, "decl: [Client-Trailer-A Client-Trailer-B], vals: valuea, valueb"); err != nil { | |
| t.Error(err) | |
| } | |
| } | |
| // Tests that servers send trailers to a client and that the client can read them. | |
| func TestTrailersServerToClient(t *testing.T) { | |
| run(t, func(t *testing.T, mode testMode) { | |
| testTrailersServerToClient(t, mode, false) | |
| }) | |
| } | |
| func TestTrailersServerToClientFlush(t *testing.T) { | |
| run(t, func(t *testing.T, mode testMode) { | |
| testTrailersServerToClient(t, mode, true) | |
| }) | |
| } | |
| func testTrailersServerToClient(t *testing.T, mode testMode, flush bool) { | |
| const body = "Some body" | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| w.Header().Set("Trailer", "Server-Trailer-A, Server-Trailer-B") | |
| w.Header().Add("Trailer", "Server-Trailer-C") | |
| io.WriteString(w, body) | |
| if flush { | |
| w.(Flusher).Flush() | |
| } | |
| // How handlers set Trailers: declare it ahead of time | |
| // with the Trailer header, and then mutate the | |
| // Header() of those values later, after the response | |
| // has been written (we wrote to w above). | |
| w.Header().Set("Server-Trailer-A", "valuea") | |
| w.Header().Set("Server-Trailer-C", "valuec") // skipping B | |
| w.Header().Set("Server-Trailer-NotDeclared", "should be omitted") | |
| })) | |
| res, err := cst.c.Get(cst.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| wantHeader := Header{ | |
| "Content-Type": {"text/plain; charset=utf-8"}, | |
| } | |
| wantLen := -1 | |
| if mode == http2Mode && !flush { | |
| // In HTTP/1.1, any use of trailers forces HTTP/1.1 | |
| // chunking and a flush at the first write. That's | |
| // unnecessary with HTTP/2's framing, so the server | |
| // is able to calculate the length while still sending | |
| // trailers afterwards. | |
| wantLen = len(body) | |
| wantHeader["Content-Length"] = []string{fmt.Sprint(wantLen)} | |
| } | |
| if res.ContentLength != int64(wantLen) { | |
| t.Errorf("ContentLength = %v; want %v", res.ContentLength, wantLen) | |
| } | |
| delete(res.Header, "Date") // irrelevant for test | |
| if !reflect.DeepEqual(res.Header, wantHeader) { | |
| t.Errorf("Header = %v; want %v", res.Header, wantHeader) | |
| } | |
| if got, want := res.Trailer, (Header{ | |
| "Server-Trailer-A": nil, | |
| "Server-Trailer-B": nil, | |
| "Server-Trailer-C": nil, | |
| }); !reflect.DeepEqual(got, want) { | |
| t.Errorf("Trailer before body read = %v; want %v", got, want) | |
| } | |
| if err := wantBody(res, nil, body); err != nil { | |
| t.Fatal(err) | |
| } | |
| if got, want := res.Trailer, (Header{ | |
| "Server-Trailer-A": {"valuea"}, | |
| "Server-Trailer-B": nil, | |
| "Server-Trailer-C": {"valuec"}, | |
| }); !reflect.DeepEqual(got, want) { | |
| t.Errorf("Trailer after body read = %v; want %v", got, want) | |
| } | |
| } | |
| // Don't allow a Body.Read after Body.Close. Issue 13648. | |
| func TestResponseBodyReadAfterClose(t *testing.T) { run(t, testResponseBodyReadAfterClose) } | |
| func testResponseBodyReadAfterClose(t *testing.T, mode testMode) { | |
| const body = "Some body" | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| io.WriteString(w, body) | |
| })) | |
| res, err := cst.c.Get(cst.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| res.Body.Close() | |
| data, err := io.ReadAll(res.Body) | |
| if len(data) != 0 || err == nil { | |
| t.Fatalf("ReadAll returned %q, %v; want error", data, err) | |
| } | |
| } | |
| func TestConcurrentReadWriteReqBody(t *testing.T) { run(t, testConcurrentReadWriteReqBody) } | |
| func testConcurrentReadWriteReqBody(t *testing.T, mode testMode) { | |
| const reqBody = "some request body" | |
| const resBody = "some response body" | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| var wg sync.WaitGroup | |
| wg.Add(2) | |
| didRead := make(chan bool, 1) | |
| // Read in one goroutine. | |
| go func() { | |
| defer wg.Done() | |
| data, err := io.ReadAll(r.Body) | |
| if string(data) != reqBody { | |
| t.Errorf("Handler read %q; want %q", data, reqBody) | |
| } | |
| if err != nil { | |
| t.Errorf("Handler Read: %v", err) | |
| } | |
| didRead <- true | |
| }() | |
| // Write in another goroutine. | |
| go func() { | |
| defer wg.Done() | |
| if mode != http2Mode { | |
| // our HTTP/1 implementation intentionally | |
| // doesn't permit writes during read (mostly | |
| // due to it being undefined); if that is ever | |
| // relaxed, change this. | |
| <-didRead | |
| } | |
| io.WriteString(w, resBody) | |
| }() | |
| wg.Wait() | |
| })) | |
| req, _ := NewRequest("POST", cst.ts.URL, strings.NewReader(reqBody)) | |
| req.Header.Add("Expect", "100-continue") // just to complicate things | |
| res, err := cst.c.Do(req) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| data, err := io.ReadAll(res.Body) | |
| defer res.Body.Close() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if string(data) != resBody { | |
| t.Errorf("read %q; want %q", data, resBody) | |
| } | |
| } | |
| func TestConnectRequest(t *testing.T) { run(t, testConnectRequest) } | |
| func testConnectRequest(t *testing.T, mode testMode) { | |
| gotc := make(chan *Request, 1) | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| gotc <- r | |
| })) | |
| u, err := url.Parse(cst.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| tests := []struct { | |
| req *Request | |
| want string | |
| }{ | |
| { | |
| req: &Request{ | |
| Method: "CONNECT", | |
| Header: Header{}, | |
| URL: u, | |
| }, | |
| want: u.Host, | |
| }, | |
| { | |
| req: &Request{ | |
| Method: "CONNECT", | |
| Header: Header{}, | |
| URL: u, | |
| Host: "example.com:123", | |
| }, | |
| want: "example.com:123", | |
| }, | |
| } | |
| for i, tt := range tests { | |
| res, err := cst.c.Do(tt.req) | |
| if err != nil { | |
| t.Errorf("%d. RoundTrip = %v", i, err) | |
| continue | |
| } | |
| res.Body.Close() | |
| req := <-gotc | |
| if req.Method != "CONNECT" { | |
| t.Errorf("method = %q; want CONNECT", req.Method) | |
| } | |
| if req.Host != tt.want { | |
| t.Errorf("Host = %q; want %q", req.Host, tt.want) | |
| } | |
| if req.URL.Host != tt.want { | |
| t.Errorf("URL.Host = %q; want %q", req.URL.Host, tt.want) | |
| } | |
| } | |
| } | |
| func TestTransportUserAgent(t *testing.T) { run(t, testTransportUserAgent) } | |
| func testTransportUserAgent(t *testing.T, mode testMode) { | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| fmt.Fprintf(w, "%q", r.Header["User-Agent"]) | |
| })) | |
| defaultUA := func() string { | |
| switch mode { | |
| case http2Mode, http2UnencryptedMode: | |
| return `["Go-http-client/2.0"]` | |
| case http3Mode: | |
| return `["Go-http-client/3.0"]` | |
| default: | |
| return `["Go-http-client/1.1"]` | |
| } | |
| } | |
| tests := []struct { | |
| setup func(*Request) | |
| want string | |
| }{ | |
| { | |
| func(r *Request) {}, | |
| defaultUA(), | |
| }, | |
| { | |
| func(r *Request) { r.Header.Set("User-Agent", "foo/1.2.3") }, | |
| `["foo/1.2.3"]`, | |
| }, | |
| { | |
| func(r *Request) { r.Header["User-Agent"] = []string{"single", "or", "multiple"} }, | |
| `["single"]`, | |
| }, | |
| { | |
| func(r *Request) { r.Header.Set("User-Agent", "") }, | |
| `[]`, | |
| }, | |
| { | |
| func(r *Request) { r.Header["User-Agent"] = nil }, | |
| `[]`, | |
| }, | |
| } | |
| for i, tt := range tests { | |
| req, _ := NewRequest("GET", cst.ts.URL, nil) | |
| tt.setup(req) | |
| res, err := cst.c.Do(req) | |
| if err != nil { | |
| t.Errorf("%d. RoundTrip = %v", i, err) | |
| continue | |
| } | |
| slurp, err := io.ReadAll(res.Body) | |
| res.Body.Close() | |
| if err != nil { | |
| t.Errorf("%d. read body = %v", i, err) | |
| continue | |
| } | |
| if string(slurp) != tt.want { | |
| t.Errorf("%d. body mismatch.\n got: %s\nwant: %s\n", i, slurp, tt.want) | |
| } | |
| } | |
| } | |
| func TestStarRequestMethod(t *testing.T) { | |
| for _, method := range []string{"FOO", "OPTIONS"} { | |
| t.Run(method, func(t *testing.T) { | |
| run(t, func(t *testing.T, mode testMode) { | |
| testStarRequest(t, method, mode) | |
| }) | |
| }) | |
| } | |
| } | |
| func testStarRequest(t *testing.T, method string, mode testMode) { | |
| gotc := make(chan *Request, 1) | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| w.Header().Set("foo", "bar") | |
| gotc <- r | |
| w.(Flusher).Flush() | |
| })) | |
| u, err := url.Parse(cst.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| u.Path = "*" | |
| req := &Request{ | |
| Method: method, | |
| Header: Header{}, | |
| URL: u, | |
| } | |
| res, err := cst.c.Do(req) | |
| if err != nil { | |
| t.Fatalf("RoundTrip = %v", err) | |
| } | |
| res.Body.Close() | |
| wantFoo := "bar" | |
| wantLen := int64(-1) | |
| if method == "OPTIONS" { | |
| wantFoo = "" | |
| wantLen = 0 | |
| } | |
| if res.StatusCode != 200 { | |
| t.Errorf("status code = %v; want %d", res.Status, 200) | |
| } | |
| if res.ContentLength != wantLen { | |
| t.Errorf("content length = %v; want %d", res.ContentLength, wantLen) | |
| } | |
| if got := res.Header.Get("foo"); got != wantFoo { | |
| t.Errorf("response \"foo\" header = %q; want %q", got, wantFoo) | |
| } | |
| select { | |
| case req = <-gotc: | |
| default: | |
| req = nil | |
| } | |
| if req == nil { | |
| if method != "OPTIONS" { | |
| t.Fatalf("handler never got request") | |
| } | |
| return | |
| } | |
| if req.Method != method { | |
| t.Errorf("method = %q; want %q", req.Method, method) | |
| } | |
| if req.URL.Path != "*" { | |
| t.Errorf("URL.Path = %q; want *", req.URL.Path) | |
| } | |
| if req.RequestURI != "*" { | |
| t.Errorf("RequestURI = %q; want *", req.RequestURI) | |
| } | |
| } | |
| // Issue 13957 | |
| func TestTransportDiscardsUnneededConns(t *testing.T) { | |
| runSynctest(t, testTransportDiscardsUnneededConns, []testMode{http2Mode}) | |
| } | |
| func testTransportDiscardsUnneededConns(t *testing.T, mode testMode) { | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| fmt.Fprintf(w, "Hello, %v", r.RemoteAddr) | |
| })) | |
| defer cst.close() | |
| var connsMu sync.Mutex | |
| var conns []*nettest.Conn | |
| cst.setDialNettestHook(func(nc *nettest.Conn) { | |
| connsMu.Lock() | |
| defer connsMu.Unlock() | |
| conns = append(conns, nc) | |
| }) | |
| var wg sync.WaitGroup | |
| for range 10 { | |
| wg.Go(func() { | |
| resp, err := cst.c.Get(cst.ts.URL) | |
| if err != nil { | |
| t.Errorf("Get: %v", err) | |
| } | |
| defer resp.Body.Close() | |
| _, err = io.ReadAll(resp.Body) | |
| if err != nil { | |
| t.Error(err) | |
| } | |
| }) | |
| } | |
| wg.Wait() | |
| synctest.Wait() | |
| open := len(conns) | |
| close := 0 | |
| for _, nc := range conns { | |
| if nc.IsClosed() { | |
| close++ | |
| } | |
| } | |
| if close != open-1 { | |
| t.Errorf("%d connections opened, %d closed; want %d to close", open, close, open-1) | |
| } | |
| } | |
| // tests that Transport doesn't retain a pointer to the provided request. | |
| func TestTransportGCRequest(t *testing.T) { | |
| run(t, func(t *testing.T, mode testMode) { | |
| t.Run("Body", func(t *testing.T) { testTransportGCRequest(t, mode, true) }) | |
| t.Run("NoBody", func(t *testing.T) { testTransportGCRequest(t, mode, false) }) | |
| }) | |
| } | |
| func testTransportGCRequest(t *testing.T, mode testMode, body bool) { | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| io.ReadAll(r.Body) | |
| if body { | |
| io.WriteString(w, "Hello.") | |
| } | |
| })) | |
| didGC := make(chan struct{}) | |
| (func() { | |
| body := strings.NewReader("some body") | |
| req, _ := NewRequest("POST", cst.ts.URL, body) | |
| runtime.AddCleanup(req, func(ch chan struct{}) { close(ch) }, didGC) | |
| res, err := cst.c.Do(req) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if _, err := io.ReadAll(res.Body); err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := res.Body.Close(); err != nil { | |
| t.Fatal(err) | |
| } | |
| })() | |
| for { | |
| select { | |
| case <-didGC: | |
| return | |
| case <-time.After(1 * time.Millisecond): | |
| runtime.GC() | |
| } | |
| } | |
| } | |
| func TestTransportRejectsInvalidHeaders(t *testing.T) { | |
| run(t, testTransportRejectsInvalidHeaders, http3SkippedMode) | |
| } | |
| func testTransportRejectsInvalidHeaders(t *testing.T, mode testMode) { | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| fmt.Fprintf(w, "Handler saw headers: %q", r.Header) | |
| }), optQuietLog) | |
| cst.tr.DisableKeepAlives = true | |
| tests := []struct { | |
| key, val string | |
| ok bool | |
| }{ | |
| {"Foo", "capital-key", true}, // verify h2 allows capital keys | |
| {"Foo", "foo\x00bar", false}, // \x00 byte in value not allowed | |
| {"Foo", "two\nlines", false}, // \n byte in value not allowed | |
| {"bogus\nkey", "v", false}, // \n byte also not allowed in key | |
| {"A space", "v", false}, // spaces in keys not allowed | |
| {"имя", "v", false}, // key must be ascii | |
| {"name", "валю", true}, // value may be non-ascii | |
| {"", "v", false}, // key must be non-empty | |
| {"k", "", true}, // value may be empty | |
| } | |
| for _, tt := range tests { | |
| dialedc := make(chan bool, 1) | |
| cst.tr.Dial = func(netw, addr string) (net.Conn, error) { | |
| dialedc <- true | |
| return net.Dial(netw, addr) | |
| } | |
| req, _ := NewRequest("GET", cst.ts.URL, nil) | |
| req.Header[tt.key] = []string{tt.val} | |
| res, err := cst.c.Do(req) | |
| var body []byte | |
| if err == nil { | |
| body, _ = io.ReadAll(res.Body) | |
| res.Body.Close() | |
| } | |
| var dialed bool | |
| select { | |
| case <-dialedc: | |
| dialed = true | |
| default: | |
| } | |
| if !tt.ok && dialed { | |
| t.Errorf("For key %q, value %q, transport dialed. Expected local failure. Response was: (%v, %v)\nServer replied with: %s", tt.key, tt.val, res, err, body) | |
| } else if (err == nil) != tt.ok { | |
| t.Errorf("For key %q, value %q; got err = %v; want ok=%v", tt.key, tt.val, err, tt.ok) | |
| } | |
| } | |
| } | |
| func TestInterruptWithPanic(t *testing.T) { | |
| run(t, func(t *testing.T, mode testMode) { | |
| for _, test := range []struct { | |
| name string | |
| panicValue any | |
| }{ | |
| {"boom", "boom"}, | |
| {"nil", nil}, | |
| {"ErrAbortHandler", ErrAbortHandler}, | |
| } { | |
| t.Run(test.name, func(t *testing.T) { | |
| synctest.Test(t, func(t *testing.T) { | |
| if test.panicValue == nil { | |
| t.Setenv("GODEBUG", "panicnil=1") | |
| } | |
| testInterruptWithPanic(t, mode, test.panicValue) | |
| }) | |
| }) | |
| } | |
| }, testNotParallel, http3SkippedMode) | |
| } | |
| func testInterruptWithPanic(t *testing.T, mode testMode, panicValue any) { | |
| const msg = "hello" | |
| testDone := make(chan struct{}) | |
| defer close(testDone) | |
| var errorLog lockedBytesBuffer | |
| gotHeaders := make(chan bool, 1) | |
| cst := newClientServerTest(t, mode, nil) | |
| cst.ts.Config.Handler = HandlerFunc(func(w ResponseWriter, r *Request) { | |
| io.WriteString(w, msg) | |
| w.(Flusher).Flush() | |
| select { | |
| case <-gotHeaders: | |
| case <-testDone: | |
| } | |
| panic(panicValue) | |
| }) | |
| cst.ts.Config.ErrorLog = log.New(&errorLog, "", 0) | |
| res, err := cst.c.Get(cst.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| gotHeaders <- true | |
| defer res.Body.Close() | |
| slurp, err := io.ReadAll(res.Body) | |
| if string(slurp) != msg { | |
| t.Errorf("client read %q; want %q", slurp, msg) | |
| } | |
| if err == nil { | |
| t.Errorf("client read all successfully; want some error") | |
| } | |
| logOutput := func() string { | |
| errorLog.Lock() | |
| defer errorLog.Unlock() | |
| return errorLog.String() | |
| } | |
| wantStackLogged := panicValue != nil && panicValue != ErrAbortHandler | |
| synctest.Wait() | |
| gotLog := logOutput() | |
| if !wantStackLogged { | |
| if gotLog != "" { | |
| t.Fatalf("want no log output; got: %s", gotLog) | |
| } | |
| } else { | |
| if gotLog == "" { | |
| t.Fatalf("wanted a stack trace logged; got nothing") | |
| } | |
| if !strings.Contains(gotLog, "created by ") && strings.Count(gotLog, "\n") < 6 { | |
| t.Fatalf("output doesn't look like a panic stack trace. Got: %s", gotLog) | |
| } | |
| } | |
| } | |
| type lockedBytesBuffer struct { | |
| sync.Mutex | |
| bytes.Buffer | |
| } | |
| func (b *lockedBytesBuffer) Write(p []byte) (int, error) { | |
| b.Lock() | |
| defer b.Unlock() | |
| return b.Buffer.Write(p) | |
| } | |
| // Issue 15366 | |
| func TestH12_AutoGzipWithDumpResponse(t *testing.T) { | |
| h12Compare{ | |
| Handler: func(w ResponseWriter, r *Request) { | |
| h := w.Header() | |
| h.Set("Content-Encoding", "gzip") | |
| h.Set("Content-Length", "23") | |
| io.WriteString(w, "\x1f\x8b\b\x00\x00\x00\x00\x00\x00\x00s\xf3\xf7\a\x00\xab'\xd4\x1a\x03\x00\x00\x00") | |
| }, | |
| EarlyCheckResponse: func(proto string, res *Response) { | |
| if !res.Uncompressed { | |
| t.Errorf("%s: expected Uncompressed to be set", proto) | |
| } | |
| dump, err := httputil.DumpResponse(res, true) | |
| if err != nil { | |
| t.Errorf("%s: DumpResponse: %v", proto, err) | |
| return | |
| } | |
| if strings.Contains(string(dump), "Connection: close") { | |
| t.Errorf("%s: should not see \"Connection: close\" in dump; got:\n%s", proto, dump) | |
| } | |
| if !strings.Contains(string(dump), "FOO") { | |
| t.Errorf("%s: should see \"FOO\" in response; got:\n%s", proto, dump) | |
| } | |
| }, | |
| }.run(t) | |
| } | |
| // Issue 14607 | |
| func TestCloseIdleConnections(t *testing.T) { run(t, testCloseIdleConnections) } | |
| func testCloseIdleConnections(t *testing.T, mode testMode) { | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| w.Header().Set("X-Addr", r.RemoteAddr) | |
| })) | |
| get := func() string { | |
| res, err := cst.c.Get(cst.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| res.Body.Close() | |
| v := res.Header.Get("X-Addr") | |
| if v == "" { | |
| t.Fatal("didn't get X-Addr") | |
| } | |
| return v | |
| } | |
| a1 := get() | |
| cst.tr.CloseIdleConnections() | |
| a2 := get() | |
| if a1 == a2 { | |
| t.Errorf("didn't close connection") | |
| } | |
| } | |
| type noteCloseConn struct { | |
| net.Conn | |
| closeFunc func() | |
| } | |
| func (x noteCloseConn) Close() error { | |
| x.closeFunc() | |
| return x.Conn.Close() | |
| } | |
| type testErrorReader struct{ t *testing.T } | |
| func (r testErrorReader) Read(p []byte) (n int, err error) { | |
| r.t.Error("unexpected Read call") | |
| return 0, io.EOF | |
| } | |
| func TestNoSniffExpectRequestBody(t *testing.T) { run(t, testNoSniffExpectRequestBody) } | |
| func testNoSniffExpectRequestBody(t *testing.T, mode testMode) { | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| w.WriteHeader(StatusUnauthorized) | |
| })) | |
| // Set ExpectContinueTimeout non-zero so RoundTrip won't try to write it. | |
| cst.tr.ExpectContinueTimeout = 10 * time.Second | |
| req, err := NewRequest("POST", cst.ts.URL, testErrorReader{t}) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| req.ContentLength = 0 // so transport is tempted to sniff it | |
| req.Header.Set("Expect", "100-continue") | |
| res, err := cst.tr.RoundTrip(req) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer res.Body.Close() | |
| if res.StatusCode != StatusUnauthorized { | |
| t.Errorf("status code = %v; want %v", res.StatusCode, StatusUnauthorized) | |
| } | |
| } | |
| func TestServerUndeclaredTrailers(t *testing.T) { run(t, testServerUndeclaredTrailers) } | |
| func testServerUndeclaredTrailers(t *testing.T, mode testMode) { | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| w.Header().Set("Foo", "Bar") | |
| w.Header().Set("Trailer:Foo", "Baz") | |
| w.(Flusher).Flush() | |
| w.Header().Add("Trailer:Foo", "Baz2") | |
| w.Header().Set("Trailer:Bar", "Quux") | |
| })) | |
| res, err := cst.c.Get(cst.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if _, err := io.Copy(io.Discard, res.Body); err != nil { | |
| t.Fatal(err) | |
| } | |
| res.Body.Close() | |
| delete(res.Header, "Date") | |
| delete(res.Header, "Content-Type") | |
| if want := (Header{"Foo": {"Bar"}}); !reflect.DeepEqual(res.Header, want) { | |
| t.Errorf("Header = %#v; want %#v", res.Header, want) | |
| } | |
| if want := (Header{"Foo": {"Baz", "Baz2"}, "Bar": {"Quux"}}); !reflect.DeepEqual(res.Trailer, want) { | |
| t.Errorf("Trailer = %#v; want %#v", res.Trailer, want) | |
| } | |
| } | |
| func TestBadResponseAfterReadingBody(t *testing.T) { | |
| run(t, testBadResponseAfterReadingBody, []testMode{http1Mode}) | |
| } | |
| func testBadResponseAfterReadingBody(t *testing.T, mode testMode) { | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| _, err := io.Copy(io.Discard, r.Body) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| c, _, err := w.(Hijacker).Hijack() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer c.Close() | |
| fmt.Fprintln(c, "some bogus crap") | |
| })) | |
| closes := 0 | |
| res, err := cst.c.Post(cst.ts.URL, "text/plain", countCloseReader{&closes, strings.NewReader("hello")}) | |
| if err == nil { | |
| res.Body.Close() | |
| t.Fatal("expected an error to be returned from Post") | |
| } | |
| if closes != 1 { | |
| t.Errorf("closes = %d; want 1", closes) | |
| } | |
| } | |
| func TestWriteHeader0(t *testing.T) { run(t, testWriteHeader0) } | |
| func testWriteHeader0(t *testing.T, mode testMode) { | |
| gotpanic := make(chan bool, 1) | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| defer close(gotpanic) | |
| defer func() { | |
| if e := recover(); e != nil { | |
| got := fmt.Sprintf("%T, %v", e, e) | |
| want := "string, invalid WriteHeader code 0" | |
| if got != want { | |
| t.Errorf("unexpected panic value:\n got: %v\nwant: %v\n", got, want) | |
| } | |
| gotpanic <- true | |
| // Set an explicit 503. This also tests that the WriteHeader call panics | |
| // before it recorded that an explicit value was set and that bogus | |
| // value wasn't stuck. | |
| w.WriteHeader(503) | |
| } | |
| }() | |
| w.WriteHeader(0) | |
| })) | |
| res, err := cst.c.Get(cst.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if res.StatusCode != 503 { | |
| t.Errorf("Response: %v %q; want 503", res.StatusCode, res.Status) | |
| } | |
| if !<-gotpanic { | |
| t.Error("expected panic in handler") | |
| } | |
| } | |
| // Issue 23010: don't be super strict checking WriteHeader's code if | |
| // it's not even valid to call WriteHeader then anyway. | |
| func TestWriteHeaderNoCodeCheck(t *testing.T) { | |
| run(t, func(t *testing.T, mode testMode) { | |
| testWriteHeaderAfterWrite(t, mode, false) | |
| }) | |
| } | |
| func TestWriteHeaderNoCodeCheck_h1hijack(t *testing.T) { | |
| testWriteHeaderAfterWrite(t, http1Mode, true) | |
| } | |
| func testWriteHeaderAfterWrite(t *testing.T, mode testMode, hijack bool) { | |
| var errorLog lockedBytesBuffer | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| if hijack { | |
| conn, _, _ := w.(Hijacker).Hijack() | |
| defer conn.Close() | |
| conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nfoo")) | |
| w.WriteHeader(0) // verify this doesn't panic if there's already output; Issue 23010 | |
| conn.Write([]byte("bar")) | |
| return | |
| } | |
| io.WriteString(w, "foo") | |
| w.(Flusher).Flush() | |
| w.WriteHeader(0) // verify this doesn't panic if there's already output; Issue 23010 | |
| io.WriteString(w, "bar") | |
| }), func(ts *httptest.Server) { | |
| ts.Config.ErrorLog = log.New(&errorLog, "", 0) | |
| }) | |
| res, err := cst.c.Get(cst.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer res.Body.Close() | |
| body, err := io.ReadAll(res.Body) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if got, want := string(body), "foobar"; got != want { | |
| t.Errorf("got = %q; want %q", got, want) | |
| } | |
| // Also check the stderr output: | |
| if mode == http2Mode || mode == http3Mode { | |
| // TODO: also emit this log message for HTTP/2? | |
| // We historically haven't, so don't check. | |
| return | |
| } | |
| gotLog := strings.TrimSpace(errorLog.String()) | |
| wantLog := "http: superfluous response.WriteHeader call from net/http_test.testWriteHeaderAfterWrite.func1 (clientserver_test.go:" | |
| if hijack { | |
| wantLog = "http: response.WriteHeader on hijacked connection from net/http_test.testWriteHeaderAfterWrite.func1 (clientserver_test.go:" | |
| } | |
| if !strings.HasPrefix(gotLog, wantLog) { | |
| t.Errorf("stderr output = %q; want %q", gotLog, wantLog) | |
| } | |
| } | |
| func TestBidiStreamReverseProxy(t *testing.T) { | |
| run(t, testBidiStreamReverseProxy, []testMode{http2Mode}) | |
| } | |
| func testBidiStreamReverseProxy(t *testing.T, mode testMode) { | |
| backend := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| if _, err := io.Copy(w, r.Body); err != nil { | |
| log.Printf("bidi backend copy: %v", err) | |
| } | |
| })) | |
| backURL, err := url.Parse(backend.ts.URL) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| rp := httputil.NewSingleHostReverseProxy(backURL) | |
| rp.Transport = backend.tr | |
| proxy := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| rp.ServeHTTP(w, r) | |
| })) | |
| bodyRes := make(chan any, 1) // error or hash.Hash | |
| pr, pw := io.Pipe() | |
| req, _ := NewRequest("PUT", proxy.ts.URL, pr) | |
| const size = 4 << 20 | |
| go func() { | |
| h := sha1.New() | |
| _, err := io.CopyN(io.MultiWriter(h, pw), rand.Reader, size) | |
| go pw.Close() | |
| if err != nil { | |
| t.Errorf("body copy: %v", err) | |
| bodyRes <- err | |
| } else { | |
| bodyRes <- h | |
| } | |
| }() | |
| res, err := backend.c.Do(req) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer res.Body.Close() | |
| hgot := sha1.New() | |
| n, err := io.Copy(hgot, res.Body) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if n != size { | |
| t.Fatalf("got %d bytes; want %d", n, size) | |
| } | |
| select { | |
| case v := <-bodyRes: | |
| switch v := v.(type) { | |
| default: | |
| t.Fatalf("body copy: %v", err) | |
| case hash.Hash: | |
| if !bytes.Equal(v.Sum(nil), hgot.Sum(nil)) { | |
| t.Errorf("written bytes didn't match received bytes") | |
| } | |
| } | |
| case <-time.After(10 * time.Second): | |
| t.Fatal("timeout") | |
| } | |
| } | |
| // Always use HTTP/1.1 for WebSocket upgrades. | |
| func TestH12_WebSocketUpgrade(t *testing.T) { | |
| h12Compare{ | |
| Handler: func(w ResponseWriter, r *Request) { | |
| h := w.Header() | |
| h.Set("Foo", "bar") | |
| }, | |
| ReqFunc: func(c *Client, url string) (*Response, error) { | |
| req, _ := NewRequest("GET", url, nil) | |
| req.Header.Set("Connection", "Upgrade") | |
| req.Header.Set("Upgrade", "WebSocket") | |
| return c.Do(req) | |
| }, | |
| EarlyCheckResponse: func(proto string, res *Response) { | |
| if res.Proto != "HTTP/1.1" { | |
| t.Errorf("%s: expected HTTP/1.1, got %q", proto, res.Proto) | |
| } | |
| res.Proto = "HTTP/IGNORE" // skip later checks that Proto must be 1.1 vs 2.0 | |
| }, | |
| Opts: []any{ | |
| func(s *Server) { | |
| // Configure servers to support HTTP/1 and HTTP/2, | |
| // so we can verify that we use HTTP/1 | |
| // even when HTTP/2 is an option. | |
| s.Protocols = &Protocols{} | |
| s.Protocols.SetHTTP1(true) | |
| s.Protocols.SetHTTP2(true) | |
| }, | |
| }, | |
| }.run(t) | |
| } | |
| func TestIdentityTransferEncoding(t *testing.T) { run(t, testIdentityTransferEncoding) } | |
| func testIdentityTransferEncoding(t *testing.T, mode testMode) { | |
| const body = "body" | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| gotBody, _ := io.ReadAll(r.Body) | |
| if got, want := string(gotBody), body; got != want { | |
| t.Errorf("got request body = %q; want %q", got, want) | |
| } | |
| w.Header().Set("Transfer-Encoding", "identity") | |
| w.WriteHeader(StatusOK) | |
| w.(Flusher).Flush() | |
| io.WriteString(w, body) | |
| })) | |
| req, _ := NewRequest("GET", cst.ts.URL, strings.NewReader(body)) | |
| res, err := cst.c.Do(req) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer res.Body.Close() | |
| gotBody, err := io.ReadAll(res.Body) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if got, want := string(gotBody), body; got != want { | |
| t.Errorf("got response body = %q; want %q", got, want) | |
| } | |
| } | |
| func TestEarlyHintsRequest(t *testing.T) { run(t, testEarlyHintsRequest) } | |
| func testEarlyHintsRequest(t *testing.T, mode testMode) { | |
| var wg sync.WaitGroup | |
| wg.Add(1) | |
| cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { | |
| h := w.Header() | |
| h.Add("Content-Length", "123") // must be ignored | |
| h.Add("Link", "</style.css>; rel=preload; as=style") | |
| h.Add("Link", "</script.js>; rel=preload; as=script") | |
| w.WriteHeader(StatusEarlyHints) | |
| wg.Wait() | |
| h.Add("Link", "</foo.js>; rel=preload; as=script") | |
| w.WriteHeader(StatusEarlyHints) | |
| w.Write([]byte("Hello")) | |
| })) | |
| checkLinkHeaders := func(t *testing.T, expected, got []string) { | |
| t.Helper() | |
| if len(expected) != len(got) { | |
| t.Errorf("got %d expected %d", len(got), len(expected)) | |
| } | |
| for i := range expected { | |
| if expected[i] != got[i] { | |
| t.Errorf("got %q expected %q", got[i], expected[i]) | |
| } | |
| } | |
| } | |
| checkExcludedHeaders := func(t *testing.T, header textproto.MIMEHeader) { | |
| t.Helper() | |
| for _, h := range []string{"Content-Length", "Transfer-Encoding"} { | |
| if v, ok := header[h]; ok { | |
| t.Errorf("%s is %q; must not be sent", h, v) | |
| } | |
| } | |
| } | |
| var respCounter uint8 | |
| trace := &httptrace.ClientTrace{ | |
| Got1xxResponse: func(code int, header textproto.MIMEHeader) error { | |
| switch respCounter { | |
| case 0: | |
| checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script"}, header["Link"]) | |
| checkExcludedHeaders(t, header) | |
| wg.Done() | |
| case 1: | |
| checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script", "</foo.js>; rel=preload; as=script"}, header["Link"]) | |
| checkExcludedHeaders(t, header) | |
| default: | |
| t.Error("Unexpected 1xx response") | |
| } | |
| respCounter++ | |
| return nil | |
| }, | |
| } | |
| req, _ := NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), "GET", cst.ts.URL, nil) | |
| res, err := cst.c.Do(req) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer res.Body.Close() | |
| checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script", "</foo.js>; rel=preload; as=script"}, res.Header["Link"]) | |
| if cl := res.Header.Get("Content-Length"); cl != "123" { | |
| t.Errorf("Content-Length is %q; want 123", cl) | |
| } | |
| body, _ := io.ReadAll(res.Body) | |
| if string(body) != "Hello" { | |
| t.Errorf("Read body %q; want Hello", body) | |
| } | |
| } | |
| // TestClientServerTLSConnWrapper verifies that the Transport and Server can | |
| // negotiate an HTTP/2 connection using a net.Conn that has a | |
| // "ConnectionState() tls.ConnectionState" method but is not a *tls.Conn. | |
| func TestClientServerTLSConnWrapper(t *testing.T) { | |
| CondSkipHTTP2(t) | |
| synctest.Test(t, func(t *testing.T) { | |
| protocols := &Protocols{} | |
| protocols.SetHTTP1(true) | |
| protocols.SetHTTP2(true) | |
| li := nettest.NewListener() | |
| server := &Server{ | |
| Handler: HandlerFunc(func(w ResponseWriter, r *Request) { | |
| if r.TLS == nil { | |
| t.Fatal("server request has no TLS ConnectionState") | |
| } | |
| }), | |
| Protocols: protocols, | |
| } | |
| defer server.Close() | |
| go server.Serve(&testListener{ | |
| accept: func() (net.Conn, error) { | |
| conn, err := li.Accept() | |
| if err != nil { | |
| return nil, err | |
| } | |
| return &testTLSConn{ | |
| Conn: conn, | |
| state: tls.ConnectionState{ | |
| Version: tls.VersionTLS13, | |
| CipherSuite: tls.TLS_AES_128_GCM_SHA256, | |
| NegotiatedProtocol: "h2", | |
| }, | |
| }, nil | |
| }, | |
| close: li.Close, | |
| addr: li.Addr(), | |
| }) | |
| tr := &Transport{ | |
| DialTLS: func(network, address string) (net.Conn, error) { | |
| return &testTLSConn{ | |
| Conn: li.NewConn(), | |
| state: tls.ConnectionState{ | |
| Version: tls.VersionTLS13, | |
| CipherSuite: tls.TLS_AES_128_GCM_SHA256, | |
| NegotiatedProtocol: "h2", | |
| }, | |
| }, nil | |
| }, | |
| Protocols: protocols, | |
| } | |
| req, _ := NewRequest("GET", "https://example.tld", nil) | |
| resp, err := tr.RoundTrip(req) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| resp.Body.Close() | |
| if resp.StatusCode != 200 { | |
| t.Errorf("response status %v, want 200", resp.StatusCode) | |
| } | |
| if resp.TLS == nil { | |
| t.Fatal("server request has no TLS ConnectionState") | |
| } | |
| }) | |
| } | |
| type testListener struct { | |
| accept func() (net.Conn, error) | |
| close func() error | |
| addr net.Addr | |
| } | |
| func (li *testListener) Accept() (net.Conn, error) { return li.accept() } | |
| func (li *testListener) Close() error { return li.close() } | |
| func (li *testListener) Addr() net.Addr { return li.addr } | |
| type testTLSConn struct { | |
| net.Conn | |
| state tls.ConnectionState | |
| } | |
| func (c *testTLSConn) Handshake() error { return nil } | |
| func (c *testTLSConn) ConnectionState() tls.ConnectionState { return c.state } |