Home | History | Annotate | Download | only in http
      1 // Copyright 2010 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package http
      6 
      7 import (
      8 	"bufio"
      9 	"bytes"
     10 	"errors"
     11 	"fmt"
     12 	"io"
     13 	"io/ioutil"
     14 	"net"
     15 	"net/url"
     16 	"strings"
     17 	"testing"
     18 	"time"
     19 )
     20 
     21 type reqWriteTest struct {
     22 	Req  Request
     23 	Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
     24 
     25 	// Any of these three may be empty to skip that test.
     26 	WantWrite string // Request.Write
     27 	WantProxy string // Request.WriteProxy
     28 
     29 	WantError error // wanted error from Request.Write
     30 }
     31 
     32 var reqWriteTests = []reqWriteTest{
     33 	// HTTP/1.1 => chunked coding; no body; no trailer
     34 	0: {
     35 		Req: Request{
     36 			Method: "GET",
     37 			URL: &url.URL{
     38 				Scheme: "http",
     39 				Host:   "www.techcrunch.com",
     40 				Path:   "/",
     41 			},
     42 			Proto:      "HTTP/1.1",
     43 			ProtoMajor: 1,
     44 			ProtoMinor: 1,
     45 			Header: Header{
     46 				"Accept":           {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
     47 				"Accept-Charset":   {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},
     48 				"Accept-Encoding":  {"gzip,deflate"},
     49 				"Accept-Language":  {"en-us,en;q=0.5"},
     50 				"Keep-Alive":       {"300"},
     51 				"Proxy-Connection": {"keep-alive"},
     52 				"User-Agent":       {"Fake"},
     53 			},
     54 			Body:  nil,
     55 			Close: false,
     56 			Host:  "www.techcrunch.com",
     57 			Form:  map[string][]string{},
     58 		},
     59 
     60 		WantWrite: "GET / HTTP/1.1\r\n" +
     61 			"Host: www.techcrunch.com\r\n" +
     62 			"User-Agent: Fake\r\n" +
     63 			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
     64 			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
     65 			"Accept-Encoding: gzip,deflate\r\n" +
     66 			"Accept-Language: en-us,en;q=0.5\r\n" +
     67 			"Keep-Alive: 300\r\n" +
     68 			"Proxy-Connection: keep-alive\r\n\r\n",
     69 
     70 		WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
     71 			"Host: www.techcrunch.com\r\n" +
     72 			"User-Agent: Fake\r\n" +
     73 			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
     74 			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
     75 			"Accept-Encoding: gzip,deflate\r\n" +
     76 			"Accept-Language: en-us,en;q=0.5\r\n" +
     77 			"Keep-Alive: 300\r\n" +
     78 			"Proxy-Connection: keep-alive\r\n\r\n",
     79 	},
     80 	// HTTP/1.1 => chunked coding; body; empty trailer
     81 	1: {
     82 		Req: Request{
     83 			Method: "GET",
     84 			URL: &url.URL{
     85 				Scheme: "http",
     86 				Host:   "www.google.com",
     87 				Path:   "/search",
     88 			},
     89 			ProtoMajor:       1,
     90 			ProtoMinor:       1,
     91 			Header:           Header{},
     92 			TransferEncoding: []string{"chunked"},
     93 		},
     94 
     95 		Body: []byte("abcdef"),
     96 
     97 		WantWrite: "GET /search HTTP/1.1\r\n" +
     98 			"Host: www.google.com\r\n" +
     99 			"User-Agent: Go-http-client/1.1\r\n" +
    100 			"Transfer-Encoding: chunked\r\n\r\n" +
    101 			chunk("abcdef") + chunk(""),
    102 
    103 		WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" +
    104 			"Host: www.google.com\r\n" +
    105 			"User-Agent: Go-http-client/1.1\r\n" +
    106 			"Transfer-Encoding: chunked\r\n\r\n" +
    107 			chunk("abcdef") + chunk(""),
    108 	},
    109 	// HTTP/1.1 POST => chunked coding; body; empty trailer
    110 	2: {
    111 		Req: Request{
    112 			Method: "POST",
    113 			URL: &url.URL{
    114 				Scheme: "http",
    115 				Host:   "www.google.com",
    116 				Path:   "/search",
    117 			},
    118 			ProtoMajor:       1,
    119 			ProtoMinor:       1,
    120 			Header:           Header{},
    121 			Close:            true,
    122 			TransferEncoding: []string{"chunked"},
    123 		},
    124 
    125 		Body: []byte("abcdef"),
    126 
    127 		WantWrite: "POST /search HTTP/1.1\r\n" +
    128 			"Host: www.google.com\r\n" +
    129 			"User-Agent: Go-http-client/1.1\r\n" +
    130 			"Connection: close\r\n" +
    131 			"Transfer-Encoding: chunked\r\n\r\n" +
    132 			chunk("abcdef") + chunk(""),
    133 
    134 		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
    135 			"Host: www.google.com\r\n" +
    136 			"User-Agent: Go-http-client/1.1\r\n" +
    137 			"Connection: close\r\n" +
    138 			"Transfer-Encoding: chunked\r\n\r\n" +
    139 			chunk("abcdef") + chunk(""),
    140 	},
    141 
    142 	// HTTP/1.1 POST with Content-Length, no chunking
    143 	3: {
    144 		Req: Request{
    145 			Method: "POST",
    146 			URL: &url.URL{
    147 				Scheme: "http",
    148 				Host:   "www.google.com",
    149 				Path:   "/search",
    150 			},
    151 			ProtoMajor:    1,
    152 			ProtoMinor:    1,
    153 			Header:        Header{},
    154 			Close:         true,
    155 			ContentLength: 6,
    156 		},
    157 
    158 		Body: []byte("abcdef"),
    159 
    160 		WantWrite: "POST /search HTTP/1.1\r\n" +
    161 			"Host: www.google.com\r\n" +
    162 			"User-Agent: Go-http-client/1.1\r\n" +
    163 			"Connection: close\r\n" +
    164 			"Content-Length: 6\r\n" +
    165 			"\r\n" +
    166 			"abcdef",
    167 
    168 		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
    169 			"Host: www.google.com\r\n" +
    170 			"User-Agent: Go-http-client/1.1\r\n" +
    171 			"Connection: close\r\n" +
    172 			"Content-Length: 6\r\n" +
    173 			"\r\n" +
    174 			"abcdef",
    175 	},
    176 
    177 	// HTTP/1.1 POST with Content-Length in headers
    178 	4: {
    179 		Req: Request{
    180 			Method: "POST",
    181 			URL:    mustParseURL("http://example.com/"),
    182 			Host:   "example.com",
    183 			Header: Header{
    184 				"Content-Length": []string{"10"}, // ignored
    185 			},
    186 			ContentLength: 6,
    187 		},
    188 
    189 		Body: []byte("abcdef"),
    190 
    191 		WantWrite: "POST / HTTP/1.1\r\n" +
    192 			"Host: example.com\r\n" +
    193 			"User-Agent: Go-http-client/1.1\r\n" +
    194 			"Content-Length: 6\r\n" +
    195 			"\r\n" +
    196 			"abcdef",
    197 
    198 		WantProxy: "POST http://example.com/ HTTP/1.1\r\n" +
    199 			"Host: example.com\r\n" +
    200 			"User-Agent: Go-http-client/1.1\r\n" +
    201 			"Content-Length: 6\r\n" +
    202 			"\r\n" +
    203 			"abcdef",
    204 	},
    205 
    206 	// default to HTTP/1.1
    207 	5: {
    208 		Req: Request{
    209 			Method: "GET",
    210 			URL:    mustParseURL("/search"),
    211 			Host:   "www.google.com",
    212 		},
    213 
    214 		WantWrite: "GET /search HTTP/1.1\r\n" +
    215 			"Host: www.google.com\r\n" +
    216 			"User-Agent: Go-http-client/1.1\r\n" +
    217 			"\r\n",
    218 	},
    219 
    220 	// Request with a 0 ContentLength and a 0 byte body.
    221 	6: {
    222 		Req: Request{
    223 			Method:        "POST",
    224 			URL:           mustParseURL("/"),
    225 			Host:          "example.com",
    226 			ProtoMajor:    1,
    227 			ProtoMinor:    1,
    228 			ContentLength: 0, // as if unset by user
    229 		},
    230 
    231 		Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) },
    232 
    233 		WantWrite: "POST / HTTP/1.1\r\n" +
    234 			"Host: example.com\r\n" +
    235 			"User-Agent: Go-http-client/1.1\r\n" +
    236 			"Transfer-Encoding: chunked\r\n" +
    237 			"\r\n0\r\n\r\n",
    238 
    239 		WantProxy: "POST / HTTP/1.1\r\n" +
    240 			"Host: example.com\r\n" +
    241 			"User-Agent: Go-http-client/1.1\r\n" +
    242 			"Transfer-Encoding: chunked\r\n" +
    243 			"\r\n0\r\n\r\n",
    244 	},
    245 
    246 	// Request with a 0 ContentLength and a nil body.
    247 	7: {
    248 		Req: Request{
    249 			Method:        "POST",
    250 			URL:           mustParseURL("/"),
    251 			Host:          "example.com",
    252 			ProtoMajor:    1,
    253 			ProtoMinor:    1,
    254 			ContentLength: 0, // as if unset by user
    255 		},
    256 
    257 		Body: func() io.ReadCloser { return nil },
    258 
    259 		WantWrite: "POST / HTTP/1.1\r\n" +
    260 			"Host: example.com\r\n" +
    261 			"User-Agent: Go-http-client/1.1\r\n" +
    262 			"Content-Length: 0\r\n" +
    263 			"\r\n",
    264 
    265 		WantProxy: "POST / HTTP/1.1\r\n" +
    266 			"Host: example.com\r\n" +
    267 			"User-Agent: Go-http-client/1.1\r\n" +
    268 			"Content-Length: 0\r\n" +
    269 			"\r\n",
    270 	},
    271 
    272 	// Request with a 0 ContentLength and a 1 byte body.
    273 	8: {
    274 		Req: Request{
    275 			Method:        "POST",
    276 			URL:           mustParseURL("/"),
    277 			Host:          "example.com",
    278 			ProtoMajor:    1,
    279 			ProtoMinor:    1,
    280 			ContentLength: 0, // as if unset by user
    281 		},
    282 
    283 		Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) },
    284 
    285 		WantWrite: "POST / HTTP/1.1\r\n" +
    286 			"Host: example.com\r\n" +
    287 			"User-Agent: Go-http-client/1.1\r\n" +
    288 			"Transfer-Encoding: chunked\r\n\r\n" +
    289 			chunk("x") + chunk(""),
    290 
    291 		WantProxy: "POST / HTTP/1.1\r\n" +
    292 			"Host: example.com\r\n" +
    293 			"User-Agent: Go-http-client/1.1\r\n" +
    294 			"Transfer-Encoding: chunked\r\n\r\n" +
    295 			chunk("x") + chunk(""),
    296 	},
    297 
    298 	// Request with a ContentLength of 10 but a 5 byte body.
    299 	9: {
    300 		Req: Request{
    301 			Method:        "POST",
    302 			URL:           mustParseURL("/"),
    303 			Host:          "example.com",
    304 			ProtoMajor:    1,
    305 			ProtoMinor:    1,
    306 			ContentLength: 10, // but we're going to send only 5 bytes
    307 		},
    308 		Body:      []byte("12345"),
    309 		WantError: errors.New("http: ContentLength=10 with Body length 5"),
    310 	},
    311 
    312 	// Request with a ContentLength of 4 but an 8 byte body.
    313 	10: {
    314 		Req: Request{
    315 			Method:        "POST",
    316 			URL:           mustParseURL("/"),
    317 			Host:          "example.com",
    318 			ProtoMajor:    1,
    319 			ProtoMinor:    1,
    320 			ContentLength: 4, // but we're going to try to send 8 bytes
    321 		},
    322 		Body:      []byte("12345678"),
    323 		WantError: errors.New("http: ContentLength=4 with Body length 8"),
    324 	},
    325 
    326 	// Request with a 5 ContentLength and nil body.
    327 	11: {
    328 		Req: Request{
    329 			Method:        "POST",
    330 			URL:           mustParseURL("/"),
    331 			Host:          "example.com",
    332 			ProtoMajor:    1,
    333 			ProtoMinor:    1,
    334 			ContentLength: 5, // but we'll omit the body
    335 		},
    336 		WantError: errors.New("http: Request.ContentLength=5 with nil Body"),
    337 	},
    338 
    339 	// Request with a 0 ContentLength and a body with 1 byte content and an error.
    340 	12: {
    341 		Req: Request{
    342 			Method:        "POST",
    343 			URL:           mustParseURL("/"),
    344 			Host:          "example.com",
    345 			ProtoMajor:    1,
    346 			ProtoMinor:    1,
    347 			ContentLength: 0, // as if unset by user
    348 		},
    349 
    350 		Body: func() io.ReadCloser {
    351 			err := errors.New("Custom reader error")
    352 			errReader := &errorReader{err}
    353 			return ioutil.NopCloser(io.MultiReader(strings.NewReader("x"), errReader))
    354 		},
    355 
    356 		WantError: errors.New("Custom reader error"),
    357 	},
    358 
    359 	// Request with a 0 ContentLength and a body without content and an error.
    360 	13: {
    361 		Req: Request{
    362 			Method:        "POST",
    363 			URL:           mustParseURL("/"),
    364 			Host:          "example.com",
    365 			ProtoMajor:    1,
    366 			ProtoMinor:    1,
    367 			ContentLength: 0, // as if unset by user
    368 		},
    369 
    370 		Body: func() io.ReadCloser {
    371 			err := errors.New("Custom reader error")
    372 			errReader := &errorReader{err}
    373 			return ioutil.NopCloser(errReader)
    374 		},
    375 
    376 		WantError: errors.New("Custom reader error"),
    377 	},
    378 
    379 	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
    380 	// and doesn't add a User-Agent.
    381 	14: {
    382 		Req: Request{
    383 			Method:     "GET",
    384 			URL:        mustParseURL("/foo"),
    385 			ProtoMajor: 1,
    386 			ProtoMinor: 0,
    387 			Header: Header{
    388 				"X-Foo": []string{"X-Bar"},
    389 			},
    390 		},
    391 
    392 		WantWrite: "GET /foo HTTP/1.1\r\n" +
    393 			"Host: \r\n" +
    394 			"User-Agent: Go-http-client/1.1\r\n" +
    395 			"X-Foo: X-Bar\r\n\r\n",
    396 	},
    397 
    398 	// If no Request.Host and no Request.URL.Host, we send
    399 	// an empty Host header, and don't use
    400 	// Request.Header["Host"]. This is just testing that
    401 	// we don't change Go 1.0 behavior.
    402 	15: {
    403 		Req: Request{
    404 			Method: "GET",
    405 			Host:   "",
    406 			URL: &url.URL{
    407 				Scheme: "http",
    408 				Host:   "",
    409 				Path:   "/search",
    410 			},
    411 			ProtoMajor: 1,
    412 			ProtoMinor: 1,
    413 			Header: Header{
    414 				"Host": []string{"bad.example.com"},
    415 			},
    416 		},
    417 
    418 		WantWrite: "GET /search HTTP/1.1\r\n" +
    419 			"Host: \r\n" +
    420 			"User-Agent: Go-http-client/1.1\r\n\r\n",
    421 	},
    422 
    423 	// Opaque test #1 from golang.org/issue/4860
    424 	16: {
    425 		Req: Request{
    426 			Method: "GET",
    427 			URL: &url.URL{
    428 				Scheme: "http",
    429 				Host:   "www.google.com",
    430 				Opaque: "/%2F/%2F/",
    431 			},
    432 			ProtoMajor: 1,
    433 			ProtoMinor: 1,
    434 			Header:     Header{},
    435 		},
    436 
    437 		WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" +
    438 			"Host: www.google.com\r\n" +
    439 			"User-Agent: Go-http-client/1.1\r\n\r\n",
    440 	},
    441 
    442 	// Opaque test #2 from golang.org/issue/4860
    443 	17: {
    444 		Req: Request{
    445 			Method: "GET",
    446 			URL: &url.URL{
    447 				Scheme: "http",
    448 				Host:   "x.google.com",
    449 				Opaque: "//y.google.com/%2F/%2F/",
    450 			},
    451 			ProtoMajor: 1,
    452 			ProtoMinor: 1,
    453 			Header:     Header{},
    454 		},
    455 
    456 		WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" +
    457 			"Host: x.google.com\r\n" +
    458 			"User-Agent: Go-http-client/1.1\r\n\r\n",
    459 	},
    460 
    461 	// Testing custom case in header keys. Issue 5022.
    462 	18: {
    463 		Req: Request{
    464 			Method: "GET",
    465 			URL: &url.URL{
    466 				Scheme: "http",
    467 				Host:   "www.google.com",
    468 				Path:   "/",
    469 			},
    470 			Proto:      "HTTP/1.1",
    471 			ProtoMajor: 1,
    472 			ProtoMinor: 1,
    473 			Header: Header{
    474 				"ALL-CAPS": {"x"},
    475 			},
    476 		},
    477 
    478 		WantWrite: "GET / HTTP/1.1\r\n" +
    479 			"Host: www.google.com\r\n" +
    480 			"User-Agent: Go-http-client/1.1\r\n" +
    481 			"ALL-CAPS: x\r\n" +
    482 			"\r\n",
    483 	},
    484 
    485 	// Request with host header field; IPv6 address with zone identifier
    486 	19: {
    487 		Req: Request{
    488 			Method: "GET",
    489 			URL: &url.URL{
    490 				Host: "[fe80::1%en0]",
    491 			},
    492 		},
    493 
    494 		WantWrite: "GET / HTTP/1.1\r\n" +
    495 			"Host: [fe80::1]\r\n" +
    496 			"User-Agent: Go-http-client/1.1\r\n" +
    497 			"\r\n",
    498 	},
    499 
    500 	// Request with optional host header field; IPv6 address with zone identifier
    501 	20: {
    502 		Req: Request{
    503 			Method: "GET",
    504 			URL: &url.URL{
    505 				Host: "www.example.com",
    506 			},
    507 			Host: "[fe80::1%en0]:8080",
    508 		},
    509 
    510 		WantWrite: "GET / HTTP/1.1\r\n" +
    511 			"Host: [fe80::1]:8080\r\n" +
    512 			"User-Agent: Go-http-client/1.1\r\n" +
    513 			"\r\n",
    514 	},
    515 }
    516 
    517 func TestRequestWrite(t *testing.T) {
    518 	for i := range reqWriteTests {
    519 		tt := &reqWriteTests[i]
    520 
    521 		setBody := func() {
    522 			if tt.Body == nil {
    523 				return
    524 			}
    525 			switch b := tt.Body.(type) {
    526 			case []byte:
    527 				tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b))
    528 			case func() io.ReadCloser:
    529 				tt.Req.Body = b()
    530 			}
    531 		}
    532 		setBody()
    533 		if tt.Req.Header == nil {
    534 			tt.Req.Header = make(Header)
    535 		}
    536 
    537 		var braw bytes.Buffer
    538 		err := tt.Req.Write(&braw)
    539 		if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e {
    540 			t.Errorf("writing #%d, err = %q, want %q", i, g, e)
    541 			continue
    542 		}
    543 		if err != nil {
    544 			continue
    545 		}
    546 
    547 		if tt.WantWrite != "" {
    548 			sraw := braw.String()
    549 			if sraw != tt.WantWrite {
    550 				t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw)
    551 				continue
    552 			}
    553 		}
    554 
    555 		if tt.WantProxy != "" {
    556 			setBody()
    557 			var praw bytes.Buffer
    558 			err = tt.Req.WriteProxy(&praw)
    559 			if err != nil {
    560 				t.Errorf("WriteProxy #%d: %s", i, err)
    561 				continue
    562 			}
    563 			sraw := praw.String()
    564 			if sraw != tt.WantProxy {
    565 				t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw)
    566 				continue
    567 			}
    568 		}
    569 	}
    570 }
    571 
    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 
    704 type closeChecker struct {
    705 	io.Reader
    706 	closed bool
    707 }
    708 
    709 func (rc *closeChecker) Close() error {
    710 	rc.closed = true
    711 	return nil
    712 }
    713 
    714 // TestRequestWriteClosesBody tests that Request.Write closes its request.Body.
    715 // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer
    716 // inside a NopCloser, and that it serializes it correctly.
    717 func TestRequestWriteClosesBody(t *testing.T) {
    718 	rc := &closeChecker{Reader: strings.NewReader("my body")}
    719 	req, err := NewRequest("POST", "http://foo.com/", rc)
    720 	if err != nil {
    721 		t.Fatal(err)
    722 	}
    723 	buf := new(bytes.Buffer)
    724 	if err := req.Write(buf); err != nil {
    725 		t.Error(err)
    726 	}
    727 	if !rc.closed {
    728 		t.Error("body not closed after write")
    729 	}
    730 	expected := "POST / HTTP/1.1\r\n" +
    731 		"Host: foo.com\r\n" +
    732 		"User-Agent: Go-http-client/1.1\r\n" +
    733 		"Transfer-Encoding: chunked\r\n\r\n" +
    734 		chunk("my body") +
    735 		chunk("")
    736 	if buf.String() != expected {
    737 		t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected)
    738 	}
    739 }
    740 
    741 func chunk(s string) string {
    742 	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
    743 }
    744 
    745 func mustParseURL(s string) *url.URL {
    746 	u, err := url.Parse(s)
    747 	if err != nil {
    748 		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
    749 	}
    750 	return u
    751 }
    752 
    753 type writerFunc func([]byte) (int, error)
    754 
    755 func (f writerFunc) Write(p []byte) (int, error) { return f(p) }
    756 
    757 // TestRequestWriteError tests the Write err != nil checks in (*Request).write.
    758 func TestRequestWriteError(t *testing.T) {
    759 	failAfter, writeCount := 0, 0
    760 	errFail := errors.New("fake write failure")
    761 
    762 	// w is the buffered io.Writer to write the request to. It
    763 	// fails exactly once on its Nth Write call, as controlled by
    764 	// failAfter. It also tracks the number of calls in
    765 	// writeCount.
    766 	w := struct {
    767 		io.ByteWriter // to avoid being wrapped by a bufio.Writer
    768 		io.Writer
    769 	}{
    770 		nil,
    771 		writerFunc(func(p []byte) (n int, err error) {
    772 			writeCount++
    773 			if failAfter == 0 {
    774 				err = errFail
    775 			}
    776 			failAfter--
    777 			return len(p), err
    778 		}),
    779 	}
    780 
    781 	req, _ := NewRequest("GET", "http://example.com/", nil)
    782 	const writeCalls = 4 // number of Write calls in current implementation
    783 	sawGood := false
    784 	for n := 0; n <= writeCalls+2; n++ {
    785 		failAfter = n
    786 		writeCount = 0
    787 		err := req.Write(w)
    788 		var wantErr error
    789 		if n < writeCalls {
    790 			wantErr = errFail
    791 		}
    792 		if err != wantErr {
    793 			t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr)
    794 			continue
    795 		}
    796 		if err == nil {
    797 			sawGood = true
    798 			if writeCount != writeCalls {
    799 				t.Fatalf("writeCalls constant is outdated in test")
    800 			}
    801 		}
    802 		if writeCount > writeCalls || writeCount > n+1 {
    803 			t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount)
    804 		}
    805 	}
    806 	if !sawGood {
    807 		t.Fatalf("writeCalls constant is outdated in test")
    808 	}
    809 }
    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 }
    883