github.com/slayercat/go@v0.0.0-20170428012452-c51559813f61/src/net/http/requestwrite_test.go (about) 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 }