diff --git a/requestwrite_test.go b/requestwrite_test.go index 53dee7bd..96d6bcdc 100644 --- a/requestwrite_test.go +++ b/requestwrite_test.go @@ -12,6 +12,7 @@ import ( "io" "net" "net/url" + "slices" "strings" "testing" "testing/iotest" @@ -978,3 +979,49 @@ func (c *dumpConn) RemoteAddr() net.Addr { return nil } func (c *dumpConn) SetDeadline(t time.Time) error { return nil } func (c *dumpConn) SetReadDeadline(t time.Time) error { return nil } func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil } + +// The transfer headers take their values from the request but keep the name +// the caller wrote. +func TestRequestWriteKeepsTransferHeaderNames(t *testing.T) { + tests := []struct { + name string + method string + body io.Reader + length int64 + header Header + trailer Header + want []string // transfer header lines, in order + }{ + {"lowercase", "POST", strings.NewReader("abc"), 3, Header{"content-length": {"3"}}, nil, []string{"content-length: 3"}}, + {"uppercase with a wrong value", "POST", strings.NewReader("abc"), 3, Header{"CONTENT-LENGTH": {"99"}}, nil, []string{"CONTENT-LENGTH: 3"}}, + {"two spellings", "POST", strings.NewReader("abc"), 3, Header{"content-length": {"3"}, "Content-length": {"3"}}, nil, []string{"Content-Length: 3"}}, + {"GET drops it", "GET", nil, 0, Header{"content-length": {"3"}}, nil, nil}, + {"lowercase transfer-encoding", "POST", io.MultiReader(strings.NewReader("abc")), -1, Header{"transfer-encoding": {"chunked"}}, nil, []string{"transfer-encoding: chunked"}}, + {"lowercase trailer", "POST", io.MultiReader(strings.NewReader("abc")), -1, Header{"trailer": {"x"}}, Header{"X-Foo": nil}, []string{"Transfer-Encoding: chunked", "trailer: X-Foo"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := dummyRequest(tt.method) + req.Header, req.ContentLength, req.Trailer = tt.header, tt.length, tt.trailer + if tt.body != nil { + req.Body = io.NopCloser(tt.body) + } + var buf bytes.Buffer + if err := req.Write(&buf); err != nil { + t.Fatal(err) + } + head, _, _ := strings.Cut(buf.String(), "\r\n\r\n") + var got []string + for _, line := range strings.Split(head, "\r\n")[1:] { + name, _, _ := strings.Cut(line, ":") + switch strings.ToLower(name) { + case "content-length", "transfer-encoding", "trailer": + got = append(got, line) + } + } + if !slices.Equal(got, tt.want) { + t.Errorf("transfer headers %q, want %q\n%s", got, tt.want, head) + } + }) + } +} diff --git a/transfer.go b/transfer.go index bdb30abe..9e24cdcb 100644 --- a/transfer.go +++ b/transfer.go @@ -275,6 +275,8 @@ func (t *transferWriter) shouldSendContentLength() bool { // addHeaders adds transfer headers to an existing header object, replacing any the caller set with the same name. // Content-Length always comes from the request, as in net/http. func (t *transferWriter) addHeaders(hdrs *Header) error { + // Not setTransferHeader: the caller's Content-Length goes even when none is sent. + contentLength := headerNameFold(*hdrs, "Content-Length") deleteHeaderFold(*hdrs, "Content-Length") if t.Close && !hasToken(t.Header.get("Connection"), "close") { hdrs.Add("Connection", "close") @@ -284,7 +286,7 @@ func (t *transferWriter) addHeaders(hdrs *Header) error { // function of the sanitized field triple (Body, ContentLength, // TransferEncoding) if t.shouldSendContentLength() { - setTransferHeader(*hdrs, "Content-Length", strconv.FormatInt(t.ContentLength, 10)) + (*hdrs)[contentLength] = []string{strconv.FormatInt(t.ContentLength, 10)} } else if chunked(t.TransferEncoding) { setTransferHeader(*hdrs, "Transfer-Encoding", "chunked") } @@ -311,10 +313,30 @@ func (t *transferWriter) addHeaders(hdrs *Header) error { return nil } -// setTransferHeader sets key to value in h, removing the caller's value under any case of key. +// setTransferHeader sets key to value in h, replacing the caller's value under +// any case of key but keeping the name the caller wrote. func setTransferHeader(h Header, key, value string) { + name := headerNameFold(h, key) deleteHeaderFold(h, key) - h.Set(key, value) + h[name] = []string{value} +} + +// headerNameFold returns the name under which h has key, if h has it under +// exactly one case, and otherwise the canonical form of key. +func headerNameFold(h Header, key string) string { + name := "" + for k := range h { + if strings.EqualFold(k, key) { + if name != "" { + return CanonicalHeaderKey(key) + } + name = k + } + } + if name == "" { + return CanonicalHeaderKey(key) + } + return name } // deleteHeaderFold deletes key from h under any case.