Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions requestwrite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"io"
"net"
"net/url"
"slices"
"strings"
"testing"
"testing/iotest"
Expand Down Expand Up @@ -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)
}
})
}
}
28 changes: 25 additions & 3 deletions transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
}
Expand All @@ -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.
Expand Down
Loading