github.com/useflyent/fhttp@v0.0.0-20211004035111-333f430cfbbf/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 "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 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + 62 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + 63 "Accept-Encoding: gzip,deflate\r\n" + 64 "Accept-Language: en-us,en;q=0.5\r\n" + 65 "Host: www.techcrunch.com\r\n" + 66 "Keep-Alive: 300\r\n" + 67 "Proxy-Connection: keep-alive\r\n" + 68 "User-Agent: Fake\r\n\r\n", 69 70 WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" + 71 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + 72 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + 73 "Accept-Encoding: gzip,deflate\r\n" + 74 "Accept-Language: en-us,en;q=0.5\r\n" + 75 "Host: www.techcrunch.com\r\n" + 76 "Keep-Alive: 300\r\n" + 77 "Proxy-Connection: keep-alive\r\n" + 78 "User-Agent: Fake\r\n\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("net/http: 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 27: { // Custom Content-Length header disregarding the Body 612 Req: Request{ 613 Method: "POST", 614 URL: mustParseURL("/"), 615 Header: Header{"Content-Length": {"100"}}, 616 }, 617 WantWrite: "POST / HTTP/1.1\r\n" + 618 "Content-Length: 100\r\n" + 619 "Host: www.google.com\r\n" + 620 "User-Agent: Go-http-client/1.1\r\n\r\n", 621 WantProxy: "POST / HTTP/1.1\r\n" + 622 "Content-Length: 100\r\n" + 623 "Host: www.google.com\r\n" + 624 "User-Agent: Go-http-client/1.1\r\n\r\n", 625 }, 626 // TODO: @zMrKrabz these are sent with Transfer-Encoding headers, when they're not suppose to 627 28: { // POST with body but with no content-length header 628 Req: Request{ 629 Method: "POST", 630 URL: mustParseURL("https://www.googl.com/"), 631 Header: Header{"Content-Length": {ContentLengthDelete}}, 632 }, 633 Body: []byte("Hello World"), 634 WantWrite: "POST / HTTP/1.1\r\n" + 635 "Host: www.google.com\r\n" + 636 "User-Agent: Go-http-client/1.1\r\n\r\n" + 637 "Hello World", 638 WantProxy: "POST / HTTP/1.1\r\n" + 639 "Host: www.google.com\r\n" + 640 "User-Agent: Go-http-client/1.1\r\n\r\n" + 641 "Hello World", 642 }, 643 29: {// POST with empty content length header disregarding body 644 Req: Request{ 645 Method: "POST", 646 URL: mustParseURL("/"), 647 Header: Header{"Content-Length": {ContentLengthDelete}}, 648 Body: io.NopCloser(strings.NewReader("Hello World")), 649 }, 650 WantWrite: "POST / HTTP/1.1\r\n" + 651 "Host: www.google.com\r\n" + 652 "Content-Length:\r\n "+ 653 "User-Agent: Go-http-client/1.1\r\n\r\n" + 654 "Hello World", 655 WantProxy: "POST / HTTP/1.1\r\n" + 656 "Host: www.google.com\r\n" + 657 "User-Agent: Go-http-client/1.1\r\n\r\n" + 658 "Hello World", 659 }, 660 } 661 662 // TestRequestCustomTransferEncoding shows how to send a POST request with no content-length header 663 func TestRequestCustomTransferEncoding(t *testing.T) { 664 req, err := NewRequest("POST", "https://www.google.com", bytes.NewBuffer([]byte("Hello World\nfrom aliens of mars"))) 665 if err != nil { 666 t.Fatalf(err.Error()) 667 } 668 req.Header = Header{"Transfer-Encoding": {"chunked"}} 669 b := bytes.NewBuffer([]byte("")) 670 if err := req.Write(b); err != nil { 671 t.Fatalf(err.Error()) 672 } 673 674 expected := "POST / HTTP/1.1\r\n" + 675 "Content-Length: 31\r\n" + 676 "Host: www.google.com\r\n" + 677 "Transfer-Encoding: chunked\r\n" + 678 "User-Agent: Go-http-client/1.1\r\n\r\n" + 679 "Hello World\nfrom aliens of mars" 680 if expected != b.String() { 681 t.Fatalf("Expected:\n%s\nGot:%s\n", expected, b.String()) 682 } 683 } 684 685 func TestRequestWrite(t *testing.T) { 686 for i := range reqWriteTests { 687 tt := &reqWriteTests[i] 688 689 setBody := func() { 690 if tt.Body == nil { 691 return 692 } 693 switch b := tt.Body.(type) { 694 case []byte: 695 tt.Req.Body = io.NopCloser(bytes.NewReader(b)) 696 case func() io.ReadCloser: 697 tt.Req.Body = b() 698 } 699 } 700 setBody() 701 if tt.Req.Header == nil { 702 tt.Req.Header = make(Header) 703 } 704 705 var braw bytes.Buffer 706 err := tt.Req.Write(&braw) 707 if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e { 708 t.Errorf("writing #%d, err = %q, want %q", i, g, e) 709 continue 710 } 711 if err != nil { 712 continue 713 } 714 715 if tt.WantWrite != "" { 716 sraw := braw.String() 717 if sraw != tt.WantWrite { 718 t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw) 719 continue 720 } 721 } 722 723 if tt.WantProxy != "" { 724 setBody() 725 var praw bytes.Buffer 726 err = tt.Req.WriteProxy(&praw) 727 if err != nil { 728 t.Errorf("WriteProxy #%d: %s", i, err) 729 continue 730 } 731 sraw := praw.String() 732 if sraw != tt.WantProxy { 733 t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw) 734 continue 735 } 736 } 737 } 738 } 739 740 func TestRequestWriteTransport(t *testing.T) { 741 t.Parallel() 742 743 matchSubstr := func(substr string) func(string) error { 744 return func(written string) error { 745 if !strings.Contains(written, substr) { 746 return fmt.Errorf("expected substring %q in request: %s", substr, written) 747 } 748 return nil 749 } 750 } 751 752 noContentLengthOrTransferEncoding := func(req string) error { 753 if strings.Contains(req, "Content-Length: ") { 754 return fmt.Errorf("unexpected Content-Length in request: %s", req) 755 } 756 if strings.Contains(req, "Transfer-Encoding: ") { 757 return fmt.Errorf("unexpected Transfer-Encoding in request: %s", req) 758 } 759 return nil 760 } 761 762 all := func(checks ...func(string) error) func(string) error { 763 return func(req string) error { 764 for _, c := range checks { 765 if err := c(req); err != nil { 766 return err 767 } 768 } 769 return nil 770 } 771 } 772 773 type testCase struct { 774 method string 775 clen int64 // ContentLength 776 body io.ReadCloser 777 want func(string) error 778 779 // optional: 780 init func(*testCase) 781 afterReqRead func() 782 } 783 784 tests := []testCase{ 785 { 786 method: "GET", 787 want: noContentLengthOrTransferEncoding, 788 }, 789 { 790 method: "GET", 791 body: io.NopCloser(strings.NewReader("")), 792 want: noContentLengthOrTransferEncoding, 793 }, 794 { 795 method: "GET", 796 clen: -1, 797 body: io.NopCloser(strings.NewReader("")), 798 want: noContentLengthOrTransferEncoding, 799 }, 800 // A GET with a body, with explicit content length: 801 { 802 method: "GET", 803 clen: 7, 804 body: io.NopCloser(strings.NewReader("foobody")), 805 want: all(matchSubstr("Content-Length: 7"), 806 matchSubstr("foobody")), 807 }, 808 // A GET with a body, sniffing the leading "f" from "foobody". 809 { 810 method: "GET", 811 clen: -1, 812 body: io.NopCloser(strings.NewReader("foobody")), 813 want: all(matchSubstr("Transfer-Encoding: chunked"), 814 matchSubstr("\r\n1\r\nf\r\n"), 815 matchSubstr("oobody")), 816 }, 817 // But a POST request is expected to have a body, so 818 // no sniffing happens: 819 { 820 method: "POST", 821 clen: -1, 822 body: io.NopCloser(strings.NewReader("foobody")), 823 want: all(matchSubstr("Transfer-Encoding: chunked"), 824 matchSubstr("foobody")), 825 }, 826 { 827 method: "POST", 828 clen: -1, 829 body: io.NopCloser(strings.NewReader("")), 830 want: all(matchSubstr("Transfer-Encoding: chunked")), 831 }, 832 // Verify that a blocking Request.Body doesn't block forever. 833 { 834 method: "GET", 835 clen: -1, 836 init: func(tt *testCase) { 837 pr, pw := io.Pipe() 838 tt.afterReqRead = func() { 839 pw.Close() 840 } 841 tt.body = io.NopCloser(pr) 842 }, 843 want: matchSubstr("Transfer-Encoding: chunked"), 844 }, 845 } 846 847 for i, tt := range tests { 848 if tt.init != nil { 849 tt.init(&tt) 850 } 851 req := &Request{ 852 Method: tt.method, 853 URL: &url.URL{ 854 Scheme: "http", 855 Host: "example.com", 856 }, 857 Header: make(Header), 858 ContentLength: tt.clen, 859 Body: tt.body, 860 } 861 got, err := dumpRequestOut(req, tt.afterReqRead) 862 if err != nil { 863 t.Errorf("test[%d]: %v", i, err) 864 continue 865 } 866 if err := tt.want(string(got)); err != nil { 867 t.Errorf("test[%d]: %v", i, err) 868 } 869 } 870 } 871 872 type closeChecker struct { 873 io.Reader 874 closed bool 875 } 876 877 func (rc *closeChecker) Close() error { 878 rc.closed = true 879 return nil 880 } 881 882 // TestRequestWriteClosesBody tests that Request.Write closes its request.Body. 883 // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer 884 // inside a NopCloser, and that it serializes it correctly. 885 func TestRequestWriteClosesBody(t *testing.T) { 886 rc := &closeChecker{Reader: strings.NewReader("my body")} 887 req, err := NewRequest("POST", "http://foo.com/", rc) 888 if err != nil { 889 t.Fatal(err) 890 } 891 buf := new(bytes.Buffer) 892 if err := req.Write(buf); err != nil { 893 t.Error(err) 894 } 895 if !rc.closed { 896 t.Error("body not closed after write") 897 } 898 expected := "POST / HTTP/1.1\r\n" + 899 "Host: foo.com\r\n" + 900 "User-Agent: Go-http-client/1.1\r\n" + 901 "Transfer-Encoding: chunked\r\n\r\n" + 902 chunk("my body") + 903 chunk("") 904 if buf.String() != expected { 905 t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected) 906 } 907 } 908 909 func chunk(s string) string { 910 return fmt.Sprintf("%x\r\n%s\r\n", len(s), s) 911 } 912 913 func mustParseURL(s string) *url.URL { 914 u, err := url.Parse(s) 915 if err != nil { 916 panic(fmt.Sprintf("Error parsing URL %q: %v", s, err)) 917 } 918 return u 919 } 920 921 type writerFunc func([]byte) (int, error) 922 923 func (f writerFunc) Write(p []byte) (int, error) { return f(p) } 924 925 // TestRequestWriteError tests the Write err != nil checks in (*Request).write. 926 func TestRequestWriteError(t *testing.T) { 927 failAfter, writeCount := 0, 0 928 errFail := errors.New("fake write failure") 929 930 // w is the buffered io.Writer to write the request to. It 931 // fails exactly once on its Nth Write call, as controlled by 932 // failAfter. It also tracks the number of calls in 933 // writeCount. 934 w := struct { 935 io.ByteWriter // to avoid being wrapped by a bufio.Writer 936 io.Writer 937 }{ 938 nil, 939 writerFunc(func(p []byte) (n int, err error) { 940 writeCount++ 941 if failAfter == 0 { 942 err = errFail 943 } 944 failAfter-- 945 return len(p), err 946 }), 947 } 948 949 req, _ := NewRequest("GET", "http://example.com/", nil) 950 const writeCalls = 4 // number of Write calls in current implementation 951 sawGood := false 952 for n := 0; n <= writeCalls+2; n++ { 953 failAfter = n 954 writeCount = 0 955 err := req.Write(w) 956 var wantErr error 957 if n < writeCalls { 958 wantErr = errFail 959 } 960 if err != wantErr { 961 t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr) 962 continue 963 } 964 if err == nil { 965 sawGood = true 966 if writeCount != writeCalls { 967 t.Fatalf("writeCalls constant is outdated in test") 968 } 969 } 970 if writeCount > writeCalls || writeCount > n+1 { 971 t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount) 972 } 973 } 974 if !sawGood { 975 t.Fatalf("writeCalls constant is outdated in test") 976 } 977 } 978 979 // dumpRequestOut is a modified copy of net/http/httputil.DumpRequestOut. 980 // Unlike the original, this version doesn't mutate the req.Body and 981 // try to restore it. It always dumps the whole body. 982 // And it doesn't support https. 983 func dumpRequestOut(req *Request, onReadHeaders func()) ([]byte, error) { 984 985 // Use the actual Transport code to record what we would send 986 // on the wire, but not using TCP. Use a Transport with a 987 // custom dialer that returns a fake net.Conn that waits 988 // for the full input (and recording it), and then responds 989 // with a dummy response. 990 var buf bytes.Buffer // records the output 991 pr, pw := io.Pipe() 992 defer pr.Close() 993 defer pw.Close() 994 dr := &delegateReader{c: make(chan io.Reader)} 995 996 t := &Transport{ 997 Dial: func(net, addr string) (net.Conn, error) { 998 return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil 999 }, 1000 } 1001 defer t.CloseIdleConnections() 1002 1003 // Wait for the request before replying with a dummy response: 1004 go func() { 1005 req, err := ReadRequest(bufio.NewReader(pr)) 1006 if err == nil { 1007 if onReadHeaders != nil { 1008 onReadHeaders() 1009 } 1010 // Ensure all the body is read; otherwise 1011 // we'll get a partial dump. 1012 io.Copy(io.Discard, req.Body) 1013 req.Body.Close() 1014 } 1015 dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n") 1016 }() 1017 1018 _, err := t.RoundTrip(req) 1019 if err != nil { 1020 return nil, err 1021 } 1022 return buf.Bytes(), nil 1023 } 1024 1025 // delegateReader is a reader that delegates to another reader, 1026 // once it arrives on a channel. 1027 type delegateReader struct { 1028 c chan io.Reader 1029 r io.Reader // nil until received from c 1030 } 1031 1032 func (r *delegateReader) Read(p []byte) (int, error) { 1033 if r.r == nil { 1034 r.r = <-r.c 1035 } 1036 return r.r.Read(p) 1037 } 1038 1039 // dumpConn is a net.Conn that writes to Writer and reads from Reader. 1040 type dumpConn struct { 1041 io.Writer 1042 io.Reader 1043 } 1044 1045 func (c *dumpConn) Close() error { return nil } 1046 func (c *dumpConn) LocalAddr() net.Addr { return nil } 1047 func (c *dumpConn) RemoteAddr() net.Addr { return nil } 1048 func (c *dumpConn) SetDeadline(t time.Time) error { return nil } 1049 func (c *dumpConn) SetReadDeadline(t time.Time) error { return nil } 1050 func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil }