github.com/guyezi/gofrontend@v0.0.0-20200228202240-7a62a49e62c0/libgo/go/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 // CONNECT without Opaque 517 21: { 518 Req: Request{ 519 Method: "CONNECT", 520 URL: &url.URL{ 521 Scheme: "https", // of proxy.com 522 Host: "proxy.com", 523 }, 524 }, 525 // What we used to do, locking that behavior in: 526 WantWrite: "CONNECT proxy.com HTTP/1.1\r\n" + 527 "Host: proxy.com\r\n" + 528 "User-Agent: Go-http-client/1.1\r\n" + 529 "\r\n", 530 }, 531 532 // CONNECT with Opaque 533 22: { 534 Req: Request{ 535 Method: "CONNECT", 536 URL: &url.URL{ 537 Scheme: "https", // of proxy.com 538 Host: "proxy.com", 539 Opaque: "backend:443", 540 }, 541 }, 542 WantWrite: "CONNECT backend:443 HTTP/1.1\r\n" + 543 "Host: proxy.com\r\n" + 544 "User-Agent: Go-http-client/1.1\r\n" + 545 "\r\n", 546 }, 547 548 // Verify that a nil header value doesn't get written. 549 23: { 550 Req: Request{ 551 Method: "GET", 552 URL: mustParseURL("/foo"), 553 Header: Header{ 554 "X-Foo": []string{"X-Bar"}, 555 "X-Idempotency-Key": nil, 556 }, 557 }, 558 559 WantWrite: "GET /foo HTTP/1.1\r\n" + 560 "Host: \r\n" + 561 "User-Agent: Go-http-client/1.1\r\n" + 562 "X-Foo: X-Bar\r\n\r\n", 563 }, 564 24: { 565 Req: Request{ 566 Method: "GET", 567 URL: mustParseURL("/foo"), 568 Header: Header{ 569 "X-Foo": []string{"X-Bar"}, 570 "X-Idempotency-Key": []string{}, 571 }, 572 }, 573 574 WantWrite: "GET /foo HTTP/1.1\r\n" + 575 "Host: \r\n" + 576 "User-Agent: Go-http-client/1.1\r\n" + 577 "X-Foo: X-Bar\r\n\r\n", 578 }, 579 580 25: { 581 Req: Request{ 582 Method: "GET", 583 URL: &url.URL{ 584 Host: "www.example.com", 585 RawQuery: "new\nline", // or any CTL 586 }, 587 }, 588 WantError: errors.New("net/http: can't write control character in Request.URL"), 589 }, 590 } 591 592 func TestRequestWrite(t *testing.T) { 593 for i := range reqWriteTests { 594 tt := &reqWriteTests[i] 595 596 setBody := func() { 597 if tt.Body == nil { 598 return 599 } 600 switch b := tt.Body.(type) { 601 case []byte: 602 tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b)) 603 case func() io.ReadCloser: 604 tt.Req.Body = b() 605 } 606 } 607 setBody() 608 if tt.Req.Header == nil { 609 tt.Req.Header = make(Header) 610 } 611 612 var braw bytes.Buffer 613 err := tt.Req.Write(&braw) 614 if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e { 615 t.Errorf("writing #%d, err = %q, want %q", i, g, e) 616 continue 617 } 618 if err != nil { 619 continue 620 } 621 622 if tt.WantWrite != "" { 623 sraw := braw.String() 624 if sraw != tt.WantWrite { 625 t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw) 626 continue 627 } 628 } 629 630 if tt.WantProxy != "" { 631 setBody() 632 var praw bytes.Buffer 633 err = tt.Req.WriteProxy(&praw) 634 if err != nil { 635 t.Errorf("WriteProxy #%d: %s", i, err) 636 continue 637 } 638 sraw := praw.String() 639 if sraw != tt.WantProxy { 640 t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw) 641 continue 642 } 643 } 644 } 645 } 646 647 func TestRequestWriteTransport(t *testing.T) { 648 t.Parallel() 649 650 matchSubstr := func(substr string) func(string) error { 651 return func(written string) error { 652 if !strings.Contains(written, substr) { 653 return fmt.Errorf("expected substring %q in request: %s", substr, written) 654 } 655 return nil 656 } 657 } 658 659 noContentLengthOrTransferEncoding := func(req string) error { 660 if strings.Contains(req, "Content-Length: ") { 661 return fmt.Errorf("unexpected Content-Length in request: %s", req) 662 } 663 if strings.Contains(req, "Transfer-Encoding: ") { 664 return fmt.Errorf("unexpected Transfer-Encoding in request: %s", req) 665 } 666 return nil 667 } 668 669 all := func(checks ...func(string) error) func(string) error { 670 return func(req string) error { 671 for _, c := range checks { 672 if err := c(req); err != nil { 673 return err 674 } 675 } 676 return nil 677 } 678 } 679 680 type testCase struct { 681 method string 682 clen int64 // ContentLength 683 body io.ReadCloser 684 want func(string) error 685 686 // optional: 687 init func(*testCase) 688 afterReqRead func() 689 } 690 691 tests := []testCase{ 692 { 693 method: "GET", 694 want: noContentLengthOrTransferEncoding, 695 }, 696 { 697 method: "GET", 698 body: ioutil.NopCloser(strings.NewReader("")), 699 want: noContentLengthOrTransferEncoding, 700 }, 701 { 702 method: "GET", 703 clen: -1, 704 body: ioutil.NopCloser(strings.NewReader("")), 705 want: noContentLengthOrTransferEncoding, 706 }, 707 // A GET with a body, with explicit content length: 708 { 709 method: "GET", 710 clen: 7, 711 body: ioutil.NopCloser(strings.NewReader("foobody")), 712 want: all(matchSubstr("Content-Length: 7"), 713 matchSubstr("foobody")), 714 }, 715 // A GET with a body, sniffing the leading "f" from "foobody". 716 { 717 method: "GET", 718 clen: -1, 719 body: ioutil.NopCloser(strings.NewReader("foobody")), 720 want: all(matchSubstr("Transfer-Encoding: chunked"), 721 matchSubstr("\r\n1\r\nf\r\n"), 722 matchSubstr("oobody")), 723 }, 724 // But a POST request is expected to have a body, so 725 // no sniffing happens: 726 { 727 method: "POST", 728 clen: -1, 729 body: ioutil.NopCloser(strings.NewReader("foobody")), 730 want: all(matchSubstr("Transfer-Encoding: chunked"), 731 matchSubstr("foobody")), 732 }, 733 { 734 method: "POST", 735 clen: -1, 736 body: ioutil.NopCloser(strings.NewReader("")), 737 want: all(matchSubstr("Transfer-Encoding: chunked")), 738 }, 739 // Verify that a blocking Request.Body doesn't block forever. 740 { 741 method: "GET", 742 clen: -1, 743 init: func(tt *testCase) { 744 pr, pw := io.Pipe() 745 tt.afterReqRead = func() { 746 pw.Close() 747 } 748 tt.body = ioutil.NopCloser(pr) 749 }, 750 want: matchSubstr("Transfer-Encoding: chunked"), 751 }, 752 } 753 754 for i, tt := range tests { 755 if tt.init != nil { 756 tt.init(&tt) 757 } 758 req := &Request{ 759 Method: tt.method, 760 URL: &url.URL{ 761 Scheme: "http", 762 Host: "example.com", 763 }, 764 Header: make(Header), 765 ContentLength: tt.clen, 766 Body: tt.body, 767 } 768 got, err := dumpRequestOut(req, tt.afterReqRead) 769 if err != nil { 770 t.Errorf("test[%d]: %v", i, err) 771 continue 772 } 773 if err := tt.want(string(got)); err != nil { 774 t.Errorf("test[%d]: %v", i, err) 775 } 776 } 777 } 778 779 type closeChecker struct { 780 io.Reader 781 closed bool 782 } 783 784 func (rc *closeChecker) Close() error { 785 rc.closed = true 786 return nil 787 } 788 789 // TestRequestWriteClosesBody tests that Request.Write closes its request.Body. 790 // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer 791 // inside a NopCloser, and that it serializes it correctly. 792 func TestRequestWriteClosesBody(t *testing.T) { 793 rc := &closeChecker{Reader: strings.NewReader("my body")} 794 req, err := NewRequest("POST", "http://foo.com/", rc) 795 if err != nil { 796 t.Fatal(err) 797 } 798 buf := new(bytes.Buffer) 799 if err := req.Write(buf); err != nil { 800 t.Error(err) 801 } 802 if !rc.closed { 803 t.Error("body not closed after write") 804 } 805 expected := "POST / HTTP/1.1\r\n" + 806 "Host: foo.com\r\n" + 807 "User-Agent: Go-http-client/1.1\r\n" + 808 "Transfer-Encoding: chunked\r\n\r\n" + 809 chunk("my body") + 810 chunk("") 811 if buf.String() != expected { 812 t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected) 813 } 814 } 815 816 func chunk(s string) string { 817 return fmt.Sprintf("%x\r\n%s\r\n", len(s), s) 818 } 819 820 func mustParseURL(s string) *url.URL { 821 u, err := url.Parse(s) 822 if err != nil { 823 panic(fmt.Sprintf("Error parsing URL %q: %v", s, err)) 824 } 825 return u 826 } 827 828 type writerFunc func([]byte) (int, error) 829 830 func (f writerFunc) Write(p []byte) (int, error) { return f(p) } 831 832 // TestRequestWriteError tests the Write err != nil checks in (*Request).write. 833 func TestRequestWriteError(t *testing.T) { 834 failAfter, writeCount := 0, 0 835 errFail := errors.New("fake write failure") 836 837 // w is the buffered io.Writer to write the request to. It 838 // fails exactly once on its Nth Write call, as controlled by 839 // failAfter. It also tracks the number of calls in 840 // writeCount. 841 w := struct { 842 io.ByteWriter // to avoid being wrapped by a bufio.Writer 843 io.Writer 844 }{ 845 nil, 846 writerFunc(func(p []byte) (n int, err error) { 847 writeCount++ 848 if failAfter == 0 { 849 err = errFail 850 } 851 failAfter-- 852 return len(p), err 853 }), 854 } 855 856 req, _ := NewRequest("GET", "http://example.com/", nil) 857 const writeCalls = 4 // number of Write calls in current implementation 858 sawGood := false 859 for n := 0; n <= writeCalls+2; n++ { 860 failAfter = n 861 writeCount = 0 862 err := req.Write(w) 863 var wantErr error 864 if n < writeCalls { 865 wantErr = errFail 866 } 867 if err != wantErr { 868 t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr) 869 continue 870 } 871 if err == nil { 872 sawGood = true 873 if writeCount != writeCalls { 874 t.Fatalf("writeCalls constant is outdated in test") 875 } 876 } 877 if writeCount > writeCalls || writeCount > n+1 { 878 t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount) 879 } 880 } 881 if !sawGood { 882 t.Fatalf("writeCalls constant is outdated in test") 883 } 884 } 885 886 // dumpRequestOut is a modified copy of net/http/httputil.DumpRequestOut. 887 // Unlike the original, this version doesn't mutate the req.Body and 888 // try to restore it. It always dumps the whole body. 889 // And it doesn't support https. 890 func dumpRequestOut(req *Request, onReadHeaders func()) ([]byte, error) { 891 892 // Use the actual Transport code to record what we would send 893 // on the wire, but not using TCP. Use a Transport with a 894 // custom dialer that returns a fake net.Conn that waits 895 // for the full input (and recording it), and then responds 896 // with a dummy response. 897 var buf bytes.Buffer // records the output 898 pr, pw := io.Pipe() 899 defer pr.Close() 900 defer pw.Close() 901 dr := &delegateReader{c: make(chan io.Reader)} 902 903 t := &Transport{ 904 Dial: func(net, addr string) (net.Conn, error) { 905 return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil 906 }, 907 } 908 defer t.CloseIdleConnections() 909 910 // Wait for the request before replying with a dummy response: 911 go func() { 912 req, err := ReadRequest(bufio.NewReader(pr)) 913 if err == nil { 914 if onReadHeaders != nil { 915 onReadHeaders() 916 } 917 // Ensure all the body is read; otherwise 918 // we'll get a partial dump. 919 io.Copy(ioutil.Discard, req.Body) 920 req.Body.Close() 921 } 922 dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n") 923 }() 924 925 _, err := t.RoundTrip(req) 926 if err != nil { 927 return nil, err 928 } 929 return buf.Bytes(), nil 930 } 931 932 // delegateReader is a reader that delegates to another reader, 933 // once it arrives on a channel. 934 type delegateReader struct { 935 c chan io.Reader 936 r io.Reader // nil until received from c 937 } 938 939 func (r *delegateReader) Read(p []byte) (int, error) { 940 if r.r == nil { 941 r.r = <-r.c 942 } 943 return r.r.Read(p) 944 } 945 946 // dumpConn is a net.Conn that writes to Writer and reads from Reader. 947 type dumpConn struct { 948 io.Writer 949 io.Reader 950 } 951 952 func (c *dumpConn) Close() error { return nil } 953 func (c *dumpConn) LocalAddr() net.Addr { return nil } 954 func (c *dumpConn) RemoteAddr() net.Addr { return nil } 955 func (c *dumpConn) SetDeadline(t time.Time) error { return nil } 956 func (c *dumpConn) SetReadDeadline(t time.Time) error { return nil } 957 func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil }