Skip to content

Commit 4859f6a

Browse files
committed
net/http: make NewRequest set empty Body nil, don't peek Read Body in Transport
This CL makes NewRequest set Body nil for known-zero bodies, and makes the http1 Transport not peek-Read a byte to determine whether there's a body. Background: Many fields of the Request struct have different meanings for whether they're outgoing (via the Transport) or incoming (via the Server). For outgoing requests, ContentLength and Body are documented as: // Body is the request's body. // // For client requests a nil body means the request has no // body, such as a GET request. The HTTP Client's Transport // is responsible for calling the Close method. Body io.ReadCloser // ContentLength records the length of the associated content. // The value -1 indicates that the length is unknown. // Values >= 0 indicate that the given number of bytes may // be read from Body. // For client requests, a value of 0 with a non-nil Body is // also treated as unknown. ContentLength int64 Because of the ambiguity of what ContentLength==0 means, the http1 and http2 Transports previously Read the first byte of a non-nil Body when the ContentLength was 0 to determine whether there was an actual body (with a non-zero length) and ContentLength just wasn't populated, or it was actually empty. That byte-sniff has been problematic and gross (see #17480, #17071) and was removed for http2 in a previous commit. That means, however, that users doing: req, _ := http.NewRequest("POST", url, strings.NewReader("")) ... would not send a Content-Length header in their http2 request, because the size of the reader (even though it was known, being one of the three common recognized types from NewRequest) was zero, and so the HTTP Transport thought it was simply unset. To signal explicitly-zero vs unset-zero, this CL changes NewRequest to signal explicitly-zero by setting the Body to nil, instead of the strings.NewReader("") or other zero-byte reader. This CL also removes the byte sniff from the http1 Transport, like https://golang.org/cl/31326 did for http2. Updates #17480 Updates #17071 Change-Id: I329f02f124659bf7d8bc01e2c9951ebdd236b52a Reviewed-on: https://go-review.googlesource.com/31445 Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@golang.org>
1 parent 4c1995f commit 4859f6a

File tree

5 files changed

+101
-90
lines changed

5 files changed

+101
-90
lines changed

src/net/http/request.go

Lines changed: 17 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,15 @@ func NewRequest(method, urlStr string, body io.Reader) (*Request, error) {
742742
req.ContentLength = int64(v.Len())
743743
case *strings.Reader:
744744
req.ContentLength = int64(v.Len())
745+
default:
746+
req.ContentLength = -1 // unknown
747+
}
748+
// For client requests, Request.ContentLength of 0
749+
// means either actually 0, or unknown. The only way
750+
// to explicitly say that the ContentLength is zero is
751+
// to set the Body to nil.
752+
if req.ContentLength == 0 {
753+
req.Body = nil
745754
}
746755
}
747756

@@ -1216,49 +1225,14 @@ func (r *Request) isReplayable() bool {
12161225
return false
12171226
}
12181227

1219-
// bodyAndLength reports the request's body and content length, with
1220-
// the difference from r.ContentLength being that 0 means actually
1221-
// zero, and -1 means unknown.
1222-
func (r *Request) bodyAndLength() (body io.Reader, contentLen int64) {
1223-
body = r.Body
1224-
if body == nil {
1225-
return nil, 0
1228+
// outgoingLength reports the Content-Length of this outgoing (Client) request.
1229+
// It maps 0 into -1 (unknown) when the Body is non-nil.
1230+
func (r *Request) outgoingLength() int64 {
1231+
if r.Body == nil {
1232+
return 0
12261233
}
12271234
if r.ContentLength != 0 {
1228-
return body, r.ContentLength
1229-
}
1230-
1231-
// Don't try to sniff the request body if,
1232-
// * they're using a custom transfer encoding (or specified
1233-
// chunked themselves)
1234-
// * they're not using HTTP/1.1 and can't chunk anyway (even
1235-
// though this is basically irrelevant, since this package
1236-
// only sends minimum 1.1 requests)
1237-
// * they're sending an "Expect: 100-continue" request, because
1238-
// they might get denied or redirected and try to use the same
1239-
// body elsewhere, so we shoudn't consume it.
1240-
if len(r.TransferEncoding) != 0 ||
1241-
!r.ProtoAtLeast(1, 1) ||
1242-
r.Header.Get("Expect") == "100-continue" {
1243-
return body, -1
1244-
}
1245-
1246-
// Test to see if it's actually zero or just unset.
1247-
var buf [1]byte
1248-
n, err := io.ReadFull(body, buf[:])
1249-
if err != nil && err != io.EOF {
1250-
return errorReader{err}, -1
1251-
}
1252-
1253-
if n == 1 {
1254-
// Oh, guess there is data in this Body Reader after all.
1255-
// The ContentLength field just wasn't set.
1256-
// Stich the Body back together again, re-attaching our
1257-
// consumed byte.
1258-
// TODO(bradfitz): switch to stitchByteAndReader
1259-
return io.MultiReader(bytes.NewReader(buf[:]), body), -1
1260-
}
1261-
1262-
// Body is actually empty.
1263-
return nil, 0
1235+
return r.ContentLength
1236+
}
1237+
return -1
12641238
}

src/net/http/request_test.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -497,18 +497,22 @@ func TestNewRequestContentLength(t *testing.T) {
497497
{bytes.NewReader([]byte("123")), 3},
498498
{bytes.NewBuffer([]byte("1234")), 4},
499499
{strings.NewReader("12345"), 5},
500+
{strings.NewReader(""), 0},
500501
// Not detected:
501-
{struct{ io.Reader }{strings.NewReader("xyz")}, 0},
502-
{io.NewSectionReader(strings.NewReader("x"), 0, 6), 0},
503-
{readByte(io.NewSectionReader(strings.NewReader("xy"), 0, 6)), 0},
502+
{struct{ io.Reader }{strings.NewReader("xyz")}, -1},
503+
{io.NewSectionReader(strings.NewReader("x"), 0, 6), -1},
504+
{readByte(io.NewSectionReader(strings.NewReader("xy"), 0, 6)), -1},
504505
}
505-
for _, tt := range tests {
506+
for i, tt := range tests {
506507
req, err := NewRequest("POST", "http://localhost/", tt.r)
507508
if err != nil {
508509
t.Fatal(err)
509510
}
510511
if req.ContentLength != tt.want {
511-
t.Errorf("ContentLength(%T) = %d; want %d", tt.r, req.ContentLength, tt.want)
512+
t.Errorf("test[%d]: ContentLength(%T) = %d; want %d", i, tt.r, req.ContentLength, tt.want)
513+
}
514+
if (req.ContentLength == 0) != (req.Body == nil) {
515+
t.Errorf("test[%d]: ContentLength = %d but Body non-nil is %v", i, req.ContentLength, req.Body != nil)
512516
}
513517
}
514518
}
@@ -667,11 +671,31 @@ func TestStarRequest(t *testing.T) {
667671
if err != nil {
668672
return
669673
}
674+
if req.ContentLength != 0 {
675+
t.Errorf("ContentLength = %d; want 0", req.ContentLength)
676+
}
677+
if req.Body == nil {
678+
t.Errorf("Body = nil; want non-nil")
679+
}
680+
681+
// Request.Write has Client semantics for Body/ContentLength,
682+
// where ContentLength 0 means unknown if Body is non-nil, and
683+
// thus chunking will happen unless we change semantics and
684+
// signal that we want to serialize it as exactly zero. The
685+
// only way to do that for outbound requests is with a nil
686+
// Body:
687+
clientReq := *req
688+
clientReq.Body = nil
689+
670690
var out bytes.Buffer
671-
if err := req.Write(&out); err != nil {
691+
if err := clientReq.Write(&out); err != nil {
672692
t.Fatal(err)
673693
}
674-
back, err := ReadRequest(bufio.NewReader(&out))
694+
695+
if strings.Contains(out.String(), "chunked") {
696+
t.Error("wrote chunked request; want no body")
697+
}
698+
back, err := ReadRequest(bufio.NewReader(bytes.NewReader(out.Bytes())))
675699
if err != nil {
676700
t.Fatal(err)
677701
}

src/net/http/requestwrite_test.go

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type reqWriteTest struct {
2828

2929
var reqWriteTests = []reqWriteTest{
3030
// HTTP/1.1 => chunked coding; no body; no trailer
31-
{
31+
0: {
3232
Req: Request{
3333
Method: "GET",
3434
URL: &url.URL{
@@ -75,7 +75,7 @@ var reqWriteTests = []reqWriteTest{
7575
"Proxy-Connection: keep-alive\r\n\r\n",
7676
},
7777
// HTTP/1.1 => chunked coding; body; empty trailer
78-
{
78+
1: {
7979
Req: Request{
8080
Method: "GET",
8181
URL: &url.URL{
@@ -104,7 +104,7 @@ var reqWriteTests = []reqWriteTest{
104104
chunk("abcdef") + chunk(""),
105105
},
106106
// HTTP/1.1 POST => chunked coding; body; empty trailer
107-
{
107+
2: {
108108
Req: Request{
109109
Method: "POST",
110110
URL: &url.URL{
@@ -137,7 +137,7 @@ var reqWriteTests = []reqWriteTest{
137137
},
138138

139139
// HTTP/1.1 POST with Content-Length, no chunking
140-
{
140+
3: {
141141
Req: Request{
142142
Method: "POST",
143143
URL: &url.URL{
@@ -172,7 +172,7 @@ var reqWriteTests = []reqWriteTest{
172172
},
173173

174174
// HTTP/1.1 POST with Content-Length in headers
175-
{
175+
4: {
176176
Req: Request{
177177
Method: "POST",
178178
URL: mustParseURL("http://example.com/"),
@@ -201,7 +201,7 @@ var reqWriteTests = []reqWriteTest{
201201
},
202202

203203
// default to HTTP/1.1
204-
{
204+
5: {
205205
Req: Request{
206206
Method: "GET",
207207
URL: mustParseURL("/search"),
@@ -215,7 +215,7 @@ var reqWriteTests = []reqWriteTest{
215215
},
216216

217217
// Request with a 0 ContentLength and a 0 byte body.
218-
{
218+
6: {
219219
Req: Request{
220220
Method: "POST",
221221
URL: mustParseURL("/"),
@@ -227,9 +227,32 @@ var reqWriteTests = []reqWriteTest{
227227

228228
Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) },
229229

230-
// RFC 2616 Section 14.13 says Content-Length should be specified
231-
// unless body is prohibited by the request method.
232-
// Also, nginx expects it for POST and PUT.
230+
WantWrite: "POST / HTTP/1.1\r\n" +
231+
"Host: example.com\r\n" +
232+
"User-Agent: Go-http-client/1.1\r\n" +
233+
"Transfer-Encoding: chunked\r\n" +
234+
"\r\n0\r\n\r\n",
235+
236+
WantProxy: "POST / HTTP/1.1\r\n" +
237+
"Host: example.com\r\n" +
238+
"User-Agent: Go-http-client/1.1\r\n" +
239+
"Transfer-Encoding: chunked\r\n" +
240+
"\r\n0\r\n\r\n",
241+
},
242+
243+
// Request with a 0 ContentLength and a nil body.
244+
7: {
245+
Req: Request{
246+
Method: "POST",
247+
URL: mustParseURL("/"),
248+
Host: "example.com",
249+
ProtoMajor: 1,
250+
ProtoMinor: 1,
251+
ContentLength: 0, // as if unset by user
252+
},
253+
254+
Body: func() io.ReadCloser { return nil },
255+
233256
WantWrite: "POST / HTTP/1.1\r\n" +
234257
"Host: example.com\r\n" +
235258
"User-Agent: Go-http-client/1.1\r\n" +
@@ -244,7 +267,7 @@ var reqWriteTests = []reqWriteTest{
244267
},
245268

246269
// Request with a 0 ContentLength and a 1 byte body.
247-
{
270+
8: {
248271
Req: Request{
249272
Method: "POST",
250273
URL: mustParseURL("/"),
@@ -270,7 +293,7 @@ var reqWriteTests = []reqWriteTest{
270293
},
271294

272295
// Request with a ContentLength of 10 but a 5 byte body.
273-
{
296+
9: {
274297
Req: Request{
275298
Method: "POST",
276299
URL: mustParseURL("/"),
@@ -284,7 +307,7 @@ var reqWriteTests = []reqWriteTest{
284307
},
285308

286309
// Request with a ContentLength of 4 but an 8 byte body.
287-
{
310+
10: {
288311
Req: Request{
289312
Method: "POST",
290313
URL: mustParseURL("/"),
@@ -298,7 +321,7 @@ var reqWriteTests = []reqWriteTest{
298321
},
299322

300323
// Request with a 5 ContentLength and nil body.
301-
{
324+
11: {
302325
Req: Request{
303326
Method: "POST",
304327
URL: mustParseURL("/"),
@@ -311,7 +334,7 @@ var reqWriteTests = []reqWriteTest{
311334
},
312335

313336
// Request with a 0 ContentLength and a body with 1 byte content and an error.
314-
{
337+
12: {
315338
Req: Request{
316339
Method: "POST",
317340
URL: mustParseURL("/"),
@@ -331,7 +354,7 @@ var reqWriteTests = []reqWriteTest{
331354
},
332355

333356
// Request with a 0 ContentLength and a body without content and an error.
334-
{
357+
13: {
335358
Req: Request{
336359
Method: "POST",
337360
URL: mustParseURL("/"),
@@ -352,7 +375,7 @@ var reqWriteTests = []reqWriteTest{
352375

353376
// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
354377
// and doesn't add a User-Agent.
355-
{
378+
14: {
356379
Req: Request{
357380
Method: "GET",
358381
URL: mustParseURL("/foo"),
@@ -373,7 +396,7 @@ var reqWriteTests = []reqWriteTest{
373396
// an empty Host header, and don't use
374397
// Request.Header["Host"]. This is just testing that
375398
// we don't change Go 1.0 behavior.
376-
{
399+
15: {
377400
Req: Request{
378401
Method: "GET",
379402
Host: "",
@@ -395,7 +418,7 @@ var reqWriteTests = []reqWriteTest{
395418
},
396419

397420
// Opaque test #1 from golang.org/issue/4860
398-
{
421+
16: {
399422
Req: Request{
400423
Method: "GET",
401424
URL: &url.URL{
@@ -414,7 +437,7 @@ var reqWriteTests = []reqWriteTest{
414437
},
415438

416439
// Opaque test #2 from golang.org/issue/4860
417-
{
440+
17: {
418441
Req: Request{
419442
Method: "GET",
420443
URL: &url.URL{
@@ -433,7 +456,7 @@ var reqWriteTests = []reqWriteTest{
433456
},
434457

435458
// Testing custom case in header keys. Issue 5022.
436-
{
459+
18: {
437460
Req: Request{
438461
Method: "GET",
439462
URL: &url.URL{
@@ -457,7 +480,7 @@ var reqWriteTests = []reqWriteTest{
457480
},
458481

459482
// Request with host header field; IPv6 address with zone identifier
460-
{
483+
19: {
461484
Req: Request{
462485
Method: "GET",
463486
URL: &url.URL{
@@ -472,7 +495,7 @@ var reqWriteTests = []reqWriteTest{
472495
},
473496

474497
// Request with optional host header field; IPv6 address with zone identifier
475-
{
498+
20: {
476499
Req: Request{
477500
Method: "GET",
478501
URL: &url.URL{
@@ -553,14 +576,14 @@ func (rc *closeChecker) Close() error {
553576
return nil
554577
}
555578

556-
// TestRequestWriteClosesBody tests that Request.Write does close its request.Body.
579+
// TestRequestWriteClosesBody tests that Request.Write closes its request.Body.
557580
// It also indirectly tests NewRequest and that it doesn't wrap an existing Closer
558581
// inside a NopCloser, and that it serializes it correctly.
559582
func TestRequestWriteClosesBody(t *testing.T) {
560583
rc := &closeChecker{Reader: strings.NewReader("my body")}
561584
req, _ := NewRequest("POST", "http://foo.com/", rc)
562-
if req.ContentLength != 0 {
563-
t.Errorf("got req.ContentLength %d, want 0", req.ContentLength)
585+
if req.ContentLength != -1 {
586+
t.Errorf("got req.ContentLength %d, want -1", req.ContentLength)
564587
}
565588
buf := new(bytes.Buffer)
566589
req.Write(buf)
@@ -571,12 +594,7 @@ func TestRequestWriteClosesBody(t *testing.T) {
571594
"Host: foo.com\r\n" +
572595
"User-Agent: Go-http-client/1.1\r\n" +
573596
"Transfer-Encoding: chunked\r\n\r\n" +
574-
// TODO: currently we don't buffer before chunking, so we get a
575-
// single "m" chunk before the other chunks, as this was the 1-byte
576-
// read from our MultiReader where we stitched the Body back together
577-
// after sniffing whether the Body was 0 bytes or not.
578-
chunk("m") +
579-
chunk("y body") +
597+
chunk("my body") +
580598
chunk("")
581599
if buf.String() != expected {
582600
t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected)

src/net/http/transfer.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ func newTransferWriter(r interface{}) (t *transferWriter, err error) {
6464
t.Trailer = rr.Trailer
6565
atLeastHTTP11 = rr.ProtoAtLeast(1, 1)
6666

67-
t.Body, t.ContentLength = rr.bodyAndLength()
67+
t.Body = rr.Body
68+
t.ContentLength = rr.outgoingLength()
6869
if t.Body != nil {
6970
t.BodyCloser = rr.Body
7071
}

0 commit comments

Comments
 (0)