// Copyright 2025 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 httpcommon import ( "cmp" "io" "net/http" "slices" "strings" "testing" ) func TestEncodeHeaders(t *testing.T) { type header struct { name string value string } for _, test := range []struct { name string in EncodeHeadersParam want EncodeHeadersResult wantHeaders []header disableCompression bool }{{ name: "simple request", in: EncodeHeadersParam{ Request: func() *http.Request { return must(http.NewRequest("GET", "https://example.tld/", nil)) }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "GET"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, }, }, { name: "host set from URL", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Host = "" req.URL.Host = "example.tld" return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "GET"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, }, }, { name: "chunked transfer-encoding", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("Transfer-Encoding", "chunked") // ignored return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "GET"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, }, }, { name: "connection close", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("Connection", "close") return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "GET"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, }, }, { name: "connection keep-alive", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("Connection", "keep-alive") return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "GET"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, }, }, { name: "normal connect", in: EncodeHeadersParam{ Request: func() *http.Request { return must(http.NewRequest("CONNECT", "https://example.tld/", nil)) }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "CONNECT"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, }, }, { name: "extended connect", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("CONNECT", "https://example.tld/", nil)) req.Header.Set(":protocol", "foo") return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "CONNECT"}, {":path", "/"}, {":protocol", "foo"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, }, }, { name: "trailers", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Trailer = make(http.Header) req.Trailer.Set("a", "1") req.Trailer.Set("b", "2") return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: true, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "GET"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"trailer", "A,B"}, {"user-agent", "default-user-agent"}, }, }, { name: "override user-agent", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("User-Agent", "GopherTron 9000") return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "GET"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "GopherTron 9000"}, }, }, { name: "disable user-agent", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header["User-Agent"] = nil return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "GET"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, }, }, { name: "ignore host header", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("Host", "gophers.tld/") // ignored return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "GET"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, }, }, { name: "crumble cookie header", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("Cookie", "a=b; b=c; c=d") return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "GET"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, // Cookie header is split into separate header fields. {"cookie", "a=b"}, {"cookie", "b=c"}, {"cookie", "c=d"}, }, }, { name: "post with nil body", in: EncodeHeadersParam{ Request: func() *http.Request { return must(http.NewRequest("POST", "https://example.tld/", nil)) }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "POST"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, {"content-length", "0"}, }, }, { name: "post with NoBody", in: EncodeHeadersParam{ Request: func() *http.Request { return must(http.NewRequest("POST", "https://example.tld/", http.NoBody)) }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "POST"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, {"content-length", "0"}, }, }, { name: "post with Content-Length", in: EncodeHeadersParam{ Request: func() *http.Request { type reader struct{ io.ReadCloser } req := must(http.NewRequest("POST", "https://example.tld/", reader{})) req.ContentLength = 10 return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: true, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "POST"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, {"content-length", "10"}, }, }, { name: "post with unknown Content-Length", in: EncodeHeadersParam{ Request: func() *http.Request { type reader struct{ io.ReadCloser } req := must(http.NewRequest("POST", "https://example.tld/", reader{})) return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: true, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "POST"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "gzip"}, {"user-agent", "default-user-agent"}, }, }, { name: "explicit accept-encoding", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("Accept-Encoding", "deflate") return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "GET"}, {":path", "/"}, {":scheme", "https"}, {"accept-encoding", "deflate"}, {"user-agent", "default-user-agent"}, }, }, { name: "head request", in: EncodeHeadersParam{ Request: func() *http.Request { return must(http.NewRequest("HEAD", "https://example.tld/", nil)) }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "HEAD"}, {":path", "/"}, {":scheme", "https"}, {"user-agent", "default-user-agent"}, }, }, { name: "range request", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("HEAD", "https://example.tld/", nil)) req.Header.Set("Range", "bytes=0-10") return req }(), DefaultUserAgent: "default-user-agent", }, want: EncodeHeadersResult{ HasBody: false, HasTrailers: false, }, wantHeaders: []header{ {":authority", "example.tld"}, {":method", "HEAD"}, {":path", "/"}, {":scheme", "https"}, {"user-agent", "default-user-agent"}, {"range", "bytes=0-10"}, }, }} { t.Run(test.name, func(t *testing.T) { var gotHeaders []header if IsRequestGzip(test.in.Request, test.disableCompression) { test.in.AddGzipHeader = true } got, err := EncodeHeaders(test.in, func(name, value string) { gotHeaders = append(gotHeaders, header{name, value}) }) if err != nil { t.Fatalf("EncodeHeaders = %v", err) } if got.HasBody != test.want.HasBody { t.Errorf("HasBody = %v, want %v", got.HasBody, test.want.HasBody) } if got.HasTrailers != test.want.HasTrailers { t.Errorf("HasTrailers = %v, want %v", got.HasTrailers, test.want.HasTrailers) } cmpHeader := func(a, b header) int { return cmp.Or( cmp.Compare(a.name, b.name), cmp.Compare(a.value, b.value), ) } slices.SortFunc(gotHeaders, cmpHeader) slices.SortFunc(test.wantHeaders, cmpHeader) if !slices.Equal(gotHeaders, test.wantHeaders) { t.Errorf("got headers:") for _, h := range gotHeaders { t.Errorf(" %v: %q", h.name, h.value) } t.Errorf("want headers:") for _, h := range test.wantHeaders { t.Errorf(" %v: %q", h.name, h.value) } } }) } } func TestEncodeHeaderErrors(t *testing.T) { for _, test := range []struct { name string in EncodeHeadersParam want string }{{ name: "URL is nil", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.URL = nil return req }(), }, want: "URL is nil", }, { name: "upgrade header is set", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("Upgrade", "foo") return req }(), }, want: "Upgrade", }, { name: "unsupported transfer-encoding header", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("Transfer-Encoding", "identity") return req }(), }, want: "Transfer-Encoding", }, { name: "unsupported connection header", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("Connection", "x") return req }(), }, want: "Connection", }, { name: "invalid host", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Host = "\x00.tld" return req }(), }, want: "Host", }, { name: "protocol header is set", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set(":protocol", "foo") return req }(), }, want: ":protocol", }, { name: "invalid path", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.URL.Path = "no_leading_slash" return req }(), }, want: "path", }, { name: "invalid header name", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("x\ny", "foo") return req }(), }, want: "header", }, { name: "invalid header value", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("x", "foo\nbar") return req }(), }, want: "header", }, { name: "invalid trailer", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Trailer = make(http.Header) req.Trailer.Set("x\ny", "foo") return req }(), }, want: "trailer", }, { name: "transfer-encoding trailer", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Trailer = make(http.Header) req.Trailer.Set("Transfer-Encoding", "chunked") return req }(), }, want: "Trailer", }, { name: "trailer trailer", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Trailer = make(http.Header) req.Trailer.Set("Trailer", "chunked") return req }(), }, want: "Trailer", }, { name: "content-length trailer", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Trailer = make(http.Header) req.Trailer.Set("Content-Length", "0") return req }(), }, want: "Trailer", }, { name: "too many headers", in: EncodeHeadersParam{ Request: func() *http.Request { req := must(http.NewRequest("GET", "https://example.tld/", nil)) req.Header.Set("X-Foo", strings.Repeat("x", 1000)) return req }(), PeerMaxHeaderListSize: 1000, }, want: "limit", }} { t.Run(test.name, func(t *testing.T) { _, err := EncodeHeaders(test.in, func(name, value string) {}) if err == nil { t.Fatalf("EncodeHeaders = nil, want %q", test.want) } if !strings.Contains(err.Error(), test.want) { t.Fatalf("EncodeHeaders = %q, want error containing %q", err, test.want) } }) } } func must[T any](v T, err error) T { if err != nil { panic(err) } return v } type panicReader struct{} func (panicReader) Read([]byte) (int, error) { panic("unexpected Read") } func (panicReader) Close() error { panic("unexpected Close") } func TestActualContentLength(t *testing.T) { tests := []struct { req *http.Request want int64 }{ // Verify we don't read from Body: 0: { req: &http.Request{Body: panicReader{}}, want: -1, }, // nil Body means 0, regardless of ContentLength: 1: { req: &http.Request{Body: nil, ContentLength: 5}, want: 0, }, // ContentLength is used if set. 2: { req: &http.Request{Body: panicReader{}, ContentLength: 5}, want: 5, }, // http.NoBody means 0, not -1. 3: { req: &http.Request{Body: http.NoBody}, want: 0, }, } for i, tt := range tests { got := ActualContentLength(tt.req) if got != tt.want { t.Errorf("test[%d]: got %d; want %d", i, got, tt.want) } } }