| // 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) |
| } |
| } |
| } |