Skip to content

Commit 6e36811

Browse files
committed
net/http: restore Transport's Request.Body byte sniff in limited cases
In Go 1.8, we'd removed the Transport's Request.Body one-byte-Read-sniffing to disambiguate between non-nil Request.Body with a ContentLength of 0 or -1. Previously, we tried to see whether a ContentLength of 0 meant actually zero, or just an unset by reading a single byte of the Request.Body and then stitching any read byte back together with the original Request.Body. That historically has caused many problems due to either data races, blocking forever (#17480), or losing bytes (#17071). Thus, we removed it in both HTTP/1 and HTTP/2 in Go 1.8. Unfortunately, during the Go 1.8 beta, we've found that a few people have gotten bitten by the behavior change on requests with methods typically not containing request bodies (e.g. GET, HEAD, DELETE). The most popular example is the aws-go SDK, which always set http.Request.Body to a non-nil value, even on such request methods. That was causing Go 1.8 to send such requests with Transfer-Encoding chunked bodies, with zero bytes, confusing popular servers (including but limited to AWS). This CL partially reverts the no-byte-sniffing behavior and restores it only for GET/HEAD/DELETE/etc requests, and only when there's no Transfer-Encoding set, and the Content-Length is 0 or -1. Updates #18257 (aws-go) bug And also private bug reports about non-AWS issues. Updates #18407 also, but haven't yet audited things enough to declare it fixed. Change-Id: Ie5284d3e067c181839b31faf637eee56e5738a6a Reviewed-on: https://go-review.googlesource.com/34668 Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent f419b56 commit 6e36811

File tree

3 files changed

+383
-11
lines changed

3 files changed

+383
-11
lines changed

src/net/http/request.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,18 @@ func (r *Request) ProtoAtLeast(major, minor int) bool {
341341
r.ProtoMajor == major && r.ProtoMinor >= minor
342342
}
343343

344+
// protoAtLeastOutgoing is like ProtoAtLeast, but is for outgoing
345+
// requests (see issue 18407) where these fields aren't supposed to
346+
// matter. As a minor fix for Go 1.8, at least treat (0, 0) as
347+
// matching HTTP/1.1 or HTTP/1.0. Only HTTP/1.1 is used.
348+
// TODO(bradfitz): ideally remove this whole method. It shouldn't be used.
349+
func (r *Request) protoAtLeastOutgoing(major, minor int) bool {
350+
if r.ProtoMajor == 0 && r.ProtoMinor == 0 && major == 1 && minor <= 1 {
351+
return true
352+
}
353+
return r.ProtoAtLeast(major, minor)
354+
}
355+
344356
// UserAgent returns the client's User-Agent, if sent in the request.
345357
func (r *Request) UserAgent() string {
346358
return r.Header.Get("User-Agent")
@@ -600,6 +612,12 @@ func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, wai
600612
}
601613
}
602614

615+
if bw, ok := w.(*bufio.Writer); ok && tw.FlushHeaders {
616+
if err := bw.Flush(); err != nil {
617+
return err
618+
}
619+
}
620+
603621
// Write body and trailer
604622
err = tw.WriteBody(w)
605623
if err != nil {
@@ -1299,3 +1317,18 @@ func (r *Request) outgoingLength() int64 {
12991317
}
13001318
return -1
13011319
}
1320+
1321+
// requestMethodUsuallyLacksBody reports whether the given request
1322+
// method is one that typically does not involve a request body.
1323+
// This is used by the Transport (via
1324+
// transferWriter.shouldSendChunkedRequestBody) to determine whether
1325+
// we try to test-read a byte from a non-nil Request.Body when
1326+
// Request.outgoingLength() returns -1. See the comments in
1327+
// shouldSendChunkedRequestBody.
1328+
func requestMethodUsuallyLacksBody(method string) bool {
1329+
switch method {
1330+
case "GET", "HEAD", "DELETE", "OPTIONS", "PROPFIND", "SEARCH":
1331+
return true
1332+
}
1333+
return false
1334+
}

src/net/http/requestwrite_test.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
package http
66

77
import (
8+
"bufio"
89
"bytes"
910
"errors"
1011
"fmt"
1112
"io"
1213
"io/ioutil"
14+
"net"
1315
"net/url"
1416
"strings"
1517
"testing"
18+
"time"
1619
)
1720

1821
type reqWriteTest struct {
@@ -566,6 +569,138 @@ func TestRequestWrite(t *testing.T) {
566569
}
567570
}
568571

572+
func TestRequestWriteTransport(t *testing.T) {
573+
t.Parallel()
574+
575+
matchSubstr := func(substr string) func(string) error {
576+
return func(written string) error {
577+
if !strings.Contains(written, substr) {
578+
return fmt.Errorf("expected substring %q in request: %s", substr, written)
579+
}
580+
return nil
581+
}
582+
}
583+
584+
noContentLengthOrTransferEncoding := func(req string) error {
585+
if strings.Contains(req, "Content-Length: ") {
586+
return fmt.Errorf("unexpected Content-Length in request: %s", req)
587+
}
588+
if strings.Contains(req, "Transfer-Encoding: ") {
589+
return fmt.Errorf("unexpected Transfer-Encoding in request: %s", req)
590+
}
591+
return nil
592+
}
593+
594+
all := func(checks ...func(string) error) func(string) error {
595+
return func(req string) error {
596+
for _, c := range checks {
597+
if err := c(req); err != nil {
598+
return err
599+
}
600+
}
601+
return nil
602+
}
603+
}
604+
605+
type testCase struct {
606+
method string
607+
clen int64 // ContentLength
608+
body io.ReadCloser
609+
want func(string) error
610+
611+
// optional:
612+
init func(*testCase)
613+
afterReqRead func()
614+
}
615+
616+
tests := []testCase{
617+
{
618+
method: "GET",
619+
want: noContentLengthOrTransferEncoding,
620+
},
621+
{
622+
method: "GET",
623+
body: ioutil.NopCloser(strings.NewReader("")),
624+
want: noContentLengthOrTransferEncoding,
625+
},
626+
{
627+
method: "GET",
628+
clen: -1,
629+
body: ioutil.NopCloser(strings.NewReader("")),
630+
want: noContentLengthOrTransferEncoding,
631+
},
632+
// A GET with a body, with explicit content length:
633+
{
634+
method: "GET",
635+
clen: 7,
636+
body: ioutil.NopCloser(strings.NewReader("foobody")),
637+
want: all(matchSubstr("Content-Length: 7"),
638+
matchSubstr("foobody")),
639+
},
640+
// A GET with a body, sniffing the leading "f" from "foobody".
641+
{
642+
method: "GET",
643+
clen: -1,
644+
body: ioutil.NopCloser(strings.NewReader("foobody")),
645+
want: all(matchSubstr("Transfer-Encoding: chunked"),
646+
matchSubstr("\r\n1\r\nf\r\n"),
647+
matchSubstr("oobody")),
648+
},
649+
// But a POST request is expected to have a body, so
650+
// no sniffing happens:
651+
{
652+
method: "POST",
653+
clen: -1,
654+
body: ioutil.NopCloser(strings.NewReader("foobody")),
655+
want: all(matchSubstr("Transfer-Encoding: chunked"),
656+
matchSubstr("foobody")),
657+
},
658+
{
659+
method: "POST",
660+
clen: -1,
661+
body: ioutil.NopCloser(strings.NewReader("")),
662+
want: all(matchSubstr("Transfer-Encoding: chunked")),
663+
},
664+
// Verify that a blocking Request.Body doesn't block forever.
665+
{
666+
method: "GET",
667+
clen: -1,
668+
init: func(tt *testCase) {
669+
pr, pw := io.Pipe()
670+
tt.afterReqRead = func() {
671+
pw.Close()
672+
}
673+
tt.body = ioutil.NopCloser(pr)
674+
},
675+
want: matchSubstr("Transfer-Encoding: chunked"),
676+
},
677+
}
678+
679+
for i, tt := range tests {
680+
if tt.init != nil {
681+
tt.init(&tt)
682+
}
683+
req := &Request{
684+
Method: tt.method,
685+
URL: &url.URL{
686+
Scheme: "http",
687+
Host: "example.com",
688+
},
689+
Header: make(Header),
690+
ContentLength: tt.clen,
691+
Body: tt.body,
692+
}
693+
got, err := dumpRequestOut(req, tt.afterReqRead)
694+
if err != nil {
695+
t.Errorf("test[%d]: %v", i, err)
696+
continue
697+
}
698+
if err := tt.want(string(got)); err != nil {
699+
t.Errorf("test[%d]: %v", i, err)
700+
}
701+
}
702+
}
703+
569704
type closeChecker struct {
570705
io.Reader
571706
closed bool
@@ -672,3 +807,76 @@ func TestRequestWriteError(t *testing.T) {
672807
t.Fatalf("writeCalls constant is outdated in test")
673808
}
674809
}
810+
811+
// dumpRequestOut is a modified copy of net/http/httputil.DumpRequestOut.
812+
// Unlike the original, this version doesn't mutate the req.Body and
813+
// try to restore it. It always dumps the whole body.
814+
// And it doesn't support https.
815+
func dumpRequestOut(req *Request, onReadHeaders func()) ([]byte, error) {
816+
817+
// Use the actual Transport code to record what we would send
818+
// on the wire, but not using TCP. Use a Transport with a
819+
// custom dialer that returns a fake net.Conn that waits
820+
// for the full input (and recording it), and then responds
821+
// with a dummy response.
822+
var buf bytes.Buffer // records the output
823+
pr, pw := io.Pipe()
824+
defer pr.Close()
825+
defer pw.Close()
826+
dr := &delegateReader{c: make(chan io.Reader)}
827+
828+
t := &Transport{
829+
Dial: func(net, addr string) (net.Conn, error) {
830+
return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil
831+
},
832+
}
833+
defer t.CloseIdleConnections()
834+
835+
// Wait for the request before replying with a dummy response:
836+
go func() {
837+
req, err := ReadRequest(bufio.NewReader(pr))
838+
if err == nil {
839+
if onReadHeaders != nil {
840+
onReadHeaders()
841+
}
842+
// Ensure all the body is read; otherwise
843+
// we'll get a partial dump.
844+
io.Copy(ioutil.Discard, req.Body)
845+
req.Body.Close()
846+
}
847+
dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n")
848+
}()
849+
850+
_, err := t.RoundTrip(req)
851+
if err != nil {
852+
return nil, err
853+
}
854+
return buf.Bytes(), nil
855+
}
856+
857+
// delegateReader is a reader that delegates to another reader,
858+
// once it arrives on a channel.
859+
type delegateReader struct {
860+
c chan io.Reader
861+
r io.Reader // nil until received from c
862+
}
863+
864+
func (r *delegateReader) Read(p []byte) (int, error) {
865+
if r.r == nil {
866+
r.r = <-r.c
867+
}
868+
return r.r.Read(p)
869+
}
870+
871+
// dumpConn is a net.Conn that writes to Writer and reads from Reader.
872+
type dumpConn struct {
873+
io.Writer
874+
io.Reader
875+
}
876+
877+
func (c *dumpConn) Close() error { return nil }
878+
func (c *dumpConn) LocalAddr() net.Addr { return nil }
879+
func (c *dumpConn) RemoteAddr() net.Addr { return nil }
880+
func (c *dumpConn) SetDeadline(t time.Time) error { return nil }
881+
func (c *dumpConn) SetReadDeadline(t time.Time) error { return nil }
882+
func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil }

0 commit comments

Comments
 (0)