Skip to content

Commit 01a5a83

Browse files
neildmknyszek
authored andcommitted
[release-branch.go1.19] net/http: accept HEAD requests with a body
RFC 7231 permits HEAD requests to contain a body, although it does state there are no defined semantics for payloads of HEAD requests and that some servers may reject HEAD requests with a payload. Accept HEAD requests with a body. Fix a bug where a HEAD request with a chunked body would interpret the body as the headers for the next request on the connection. For #53960. For #56154. Change-Id: I83f7112fdedabd6d6291cd956151d718ee6942cd Reviewed-on: https://go-review.googlesource.com/c/go/+/418614 Run-TryBot: Damien Neil <dneil@google.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> Reviewed-by: Cherry Mui <cherryyz@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-on: https://go-review.googlesource.com/c/go/+/457438 Reviewed-by: Than McIntosh <thanm@google.com>
1 parent 73e1aff commit 01a5a83

File tree

4 files changed

+85
-23
lines changed

4 files changed

+85
-23
lines changed

src/net/http/readrequest_test.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -450,16 +450,19 @@ Content-Length: 3
450450
Content-Length: 4
451451
452452
abc`)},
453-
{"smuggle_content_len_head", reqBytes(`HEAD / HTTP/1.1
453+
{"smuggle_two_content_len_head", reqBytes(`HEAD / HTTP/1.1
454454
Host: foo
455-
Content-Length: 5`)},
455+
Content-Length: 4
456+
Content-Length: 5
457+
458+
1234`)},
456459

457460
// golang.org/issue/22464
458461
{"leading_space_in_header", reqBytes(`HEAD / HTTP/1.1
459462
Host: foo
460463
Content-Length: 5`)},
461464
{"leading_tab_in_header", reqBytes(`HEAD / HTTP/1.1
462-
\tHost: foo
465+
` + "\t" + `Host: foo
463466
Content-Length: 5`)},
464467
}
465468

src/net/http/request_test.go

+7-11
Original file line numberDiff line numberDiff line change
@@ -485,43 +485,39 @@ var readRequestErrorTests = []struct {
485485
1: {"GET / HTTP/1.1\r\nheader:foo\r\n", io.ErrUnexpectedEOF.Error(), nil},
486486
2: {"", io.EOF.Error(), nil},
487487
3: {
488-
in: "HEAD / HTTP/1.1\r\nContent-Length:4\r\n\r\n",
489-
err: "http: method cannot contain a Content-Length",
490-
},
491-
4: {
492488
in: "HEAD / HTTP/1.1\r\n\r\n",
493489
header: Header{},
494490
},
495491

496492
// Multiple Content-Length values should either be
497493
// deduplicated if same or reject otherwise
498494
// See Issue 16490.
499-
5: {
495+
4: {
500496
in: "POST / HTTP/1.1\r\nContent-Length: 10\r\nContent-Length: 0\r\n\r\nGopher hey\r\n",
501497
err: "cannot contain multiple Content-Length headers",
502498
},
503-
6: {
499+
5: {
504500
in: "POST / HTTP/1.1\r\nContent-Length: 10\r\nContent-Length: 6\r\n\r\nGopher\r\n",
505501
err: "cannot contain multiple Content-Length headers",
506502
},
507-
7: {
503+
6: {
508504
in: "PUT / HTTP/1.1\r\nContent-Length: 6 \r\nContent-Length: 6\r\nContent-Length:6\r\n\r\nGopher\r\n",
509505
err: "",
510506
header: Header{"Content-Length": {"6"}},
511507
},
512-
8: {
508+
7: {
513509
in: "PUT / HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 6 \r\n\r\n",
514510
err: "cannot contain multiple Content-Length headers",
515511
},
516-
9: {
512+
8: {
517513
in: "POST / HTTP/1.1\r\nContent-Length:\r\nContent-Length: 3\r\n\r\n",
518514
err: "cannot contain multiple Content-Length headers",
519515
},
520-
10: {
516+
9: {
521517
in: "HEAD / HTTP/1.1\r\nContent-Length:0\r\nContent-Length: 0\r\n\r\n",
522518
header: Header{"Content-Length": {"0"}},
523519
},
524-
11: {
520+
10: {
525521
in: "HEAD / HTTP/1.1\r\nHost: foo\r\nHost: bar\r\n\r\n\r\n\r\n",
526522
err: "too many Host headers",
527523
},

src/net/http/serve_test.go

+70
Original file line numberDiff line numberDiff line change
@@ -6758,3 +6758,73 @@ func TestProcessing(t *testing.T) {
67586758
t.Errorf("unexpected response; got %q; should start by %q", got, expected)
67596759
}
67606760
}
6761+
6762+
func TestHeadBody(t *testing.T) {
6763+
const identityMode = false
6764+
const chunkedMode = true
6765+
t.Run("h1", func(t *testing.T) {
6766+
t.Run("identity", func(t *testing.T) { testHeadBody(t, h1Mode, identityMode, "HEAD") })
6767+
t.Run("chunked", func(t *testing.T) { testHeadBody(t, h1Mode, chunkedMode, "HEAD") })
6768+
})
6769+
t.Run("h2", func(t *testing.T) {
6770+
t.Run("identity", func(t *testing.T) { testHeadBody(t, h2Mode, identityMode, "HEAD") })
6771+
t.Run("chunked", func(t *testing.T) { testHeadBody(t, h2Mode, chunkedMode, "HEAD") })
6772+
})
6773+
}
6774+
6775+
func TestGetBody(t *testing.T) {
6776+
const identityMode = false
6777+
const chunkedMode = true
6778+
t.Run("h1", func(t *testing.T) {
6779+
t.Run("identity", func(t *testing.T) { testHeadBody(t, h1Mode, identityMode, "GET") })
6780+
t.Run("chunked", func(t *testing.T) { testHeadBody(t, h1Mode, chunkedMode, "GET") })
6781+
})
6782+
t.Run("h2", func(t *testing.T) {
6783+
t.Run("identity", func(t *testing.T) { testHeadBody(t, h2Mode, identityMode, "GET") })
6784+
t.Run("chunked", func(t *testing.T) { testHeadBody(t, h2Mode, chunkedMode, "GET") })
6785+
})
6786+
}
6787+
6788+
func testHeadBody(t *testing.T, h2, chunked bool, method string) {
6789+
setParallel(t)
6790+
defer afterTest(t)
6791+
cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
6792+
b, err := io.ReadAll(r.Body)
6793+
if err != nil {
6794+
t.Errorf("server reading body: %v", err)
6795+
return
6796+
}
6797+
w.Header().Set("X-Request-Body", string(b))
6798+
w.Header().Set("Content-Length", "0")
6799+
}))
6800+
defer cst.close()
6801+
for _, reqBody := range []string{
6802+
"",
6803+
"",
6804+
"request_body",
6805+
"",
6806+
} {
6807+
var bodyReader io.Reader
6808+
if reqBody != "" {
6809+
bodyReader = strings.NewReader(reqBody)
6810+
if chunked {
6811+
bodyReader = bufio.NewReader(bodyReader)
6812+
}
6813+
}
6814+
req, err := NewRequest(method, cst.ts.URL, bodyReader)
6815+
if err != nil {
6816+
t.Fatal(err)
6817+
}
6818+
res, err := cst.c.Do(req)
6819+
if err != nil {
6820+
t.Fatal(err)
6821+
}
6822+
res.Body.Close()
6823+
if got, want := res.StatusCode, 200; got != want {
6824+
t.Errorf("%v request with %d-byte body: StatusCode = %v, want %v", method, len(reqBody), got, want)
6825+
}
6826+
if got, want := res.Header.Get("X-Request-Body"), reqBody; got != want {
6827+
t.Errorf("%v request with %d-byte body: handler read body %q, want %q", method, len(reqBody), got, want)
6828+
}
6829+
}
6830+
}

src/net/http/transfer.go

+2-9
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ func readTransfer(msg any, r *bufio.Reader) (err error) {
557557
// or close connection when finished, since multipart is not supported yet
558558
switch {
559559
case t.Chunked:
560-
if noResponseBodyExpected(t.RequestMethod) || !bodyAllowedForStatus(t.StatusCode) {
560+
if isResponse && (noResponseBodyExpected(t.RequestMethod) || !bodyAllowedForStatus(t.StatusCode)) {
561561
t.Body = NoBody
562562
} else {
563563
t.Body = &body{src: internal.NewChunkedReader(r), hdr: msg, r: r, closing: t.Close}
@@ -691,14 +691,7 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header,
691691
}
692692

693693
// Logic based on response type or status
694-
if noResponseBodyExpected(requestMethod) {
695-
// For HTTP requests, as part of hardening against request
696-
// smuggling (RFC 7230), don't allow a Content-Length header for
697-
// methods which don't permit bodies. As an exception, allow
698-
// exactly one Content-Length header if its value is "0".
699-
if isRequest && len(contentLens) > 0 && !(len(contentLens) == 1 && contentLens[0] == "0") {
700-
return 0, fmt.Errorf("http: method cannot contain a Content-Length; got %q", contentLens)
701-
}
694+
if isResponse && noResponseBodyExpected(requestMethod) {
702695
return 0, nil
703696
}
704697
if status/100 == 1 {

0 commit comments

Comments
 (0)