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