gitee.com/ks-custle/core-gm@v0.0.0-20230922171213-b83bdd97b62c/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 //goland:noinspection HttpUrlsUsage,GoErrorStringFormat 33 var reqWriteTests = []reqWriteTest{ 34 // HTTP/1.1 => chunked coding; no body; no trailer 35 0: { 36 Req: Request{ 37 Method: "GET", 38 URL: &url.URL{ 39 Scheme: "http", 40 Host: "www.techcrunch.com", 41 Path: "/", 42 }, 43 Proto: "HTTP/1.1", 44 ProtoMajor: 1, 45 ProtoMinor: 1, 46 Header: Header{ 47 "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, 48 "Accept-Charset": {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"}, 49 "Accept-Encoding": {"gzip,deflate"}, 50 "Accept-Language": {"en-us,en;q=0.5"}, 51 "Keep-Alive": {"300"}, 52 "Proxy-Connection": {"keep-alive"}, 53 "User-Agent": {"Fake"}, 54 }, 55 Body: nil, 56 Close: false, 57 Host: "www.techcrunch.com", 58 Form: map[string][]string{}, 59 }, 60 61 WantWrite: "GET / HTTP/1.1\r\n" + 62 "Host: www.techcrunch.com\r\n" + 63 "User-Agent: Fake\r\n" + 64 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + 65 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + 66 "Accept-Encoding: gzip,deflate\r\n" + 67 "Accept-Language: en-us,en;q=0.5\r\n" + 68 "Keep-Alive: 300\r\n" + 69 "Proxy-Connection: keep-alive\r\n\r\n", 70 71 WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" + 72 "Host: www.techcrunch.com\r\n" + 73 "User-Agent: Fake\r\n" + 74 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + 75 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + 76 "Accept-Encoding: gzip,deflate\r\n" + 77 "Accept-Language: en-us,en;q=0.5\r\n" + 78 "Keep-Alive: 300\r\n" + 79 "Proxy-Connection: keep-alive\r\n\r\n", 80 }, 81 // HTTP/1.1 => chunked coding; body; empty trailer 82 1: { 83 Req: Request{ 84 Method: "GET", 85 URL: &url.URL{ 86 Scheme: "http", 87 Host: "www.google.com", 88 Path: "/search", 89 }, 90 ProtoMajor: 1, 91 ProtoMinor: 1, 92 Header: Header{}, 93 TransferEncoding: []string{"chunked"}, 94 }, 95 96 Body: []byte("abcdef"), 97 98 WantWrite: "GET /search HTTP/1.1\r\n" + 99 "Host: www.google.com\r\n" + 100 "User-Agent: Go-http-client/1.1\r\n" + 101 "Transfer-Encoding: chunked\r\n\r\n" + 102 chunk("abcdef") + chunk(""), 103 104 WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" + 105 "Host: www.google.com\r\n" + 106 "User-Agent: Go-http-client/1.1\r\n" + 107 "Transfer-Encoding: chunked\r\n\r\n" + 108 chunk("abcdef") + chunk(""), 109 }, 110 // HTTP/1.1 POST => chunked coding; body; empty trailer 111 2: { 112 Req: Request{ 113 Method: "POST", 114 URL: &url.URL{ 115 Scheme: "http", 116 Host: "www.google.com", 117 Path: "/search", 118 }, 119 ProtoMajor: 1, 120 ProtoMinor: 1, 121 Header: Header{}, 122 Close: true, 123 TransferEncoding: []string{"chunked"}, 124 }, 125 126 Body: []byte("abcdef"), 127 128 WantWrite: "POST /search HTTP/1.1\r\n" + 129 "Host: www.google.com\r\n" + 130 "User-Agent: Go-http-client/1.1\r\n" + 131 "Connection: close\r\n" + 132 "Transfer-Encoding: chunked\r\n\r\n" + 133 chunk("abcdef") + chunk(""), 134 135 WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" + 136 "Host: www.google.com\r\n" + 137 "User-Agent: Go-http-client/1.1\r\n" + 138 "Connection: close\r\n" + 139 "Transfer-Encoding: chunked\r\n\r\n" + 140 chunk("abcdef") + chunk(""), 141 }, 142 143 // HTTP/1.1 POST with Content-Length, no chunking 144 3: { 145 Req: Request{ 146 Method: "POST", 147 URL: &url.URL{ 148 Scheme: "http", 149 Host: "www.google.com", 150 Path: "/search", 151 }, 152 ProtoMajor: 1, 153 ProtoMinor: 1, 154 Header: Header{}, 155 Close: true, 156 ContentLength: 6, 157 }, 158 159 Body: []byte("abcdef"), 160 161 WantWrite: "POST /search HTTP/1.1\r\n" + 162 "Host: www.google.com\r\n" + 163 "User-Agent: Go-http-client/1.1\r\n" + 164 "Connection: close\r\n" + 165 "Content-Length: 6\r\n" + 166 "\r\n" + 167 "abcdef", 168 169 WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" + 170 "Host: www.google.com\r\n" + 171 "User-Agent: Go-http-client/1.1\r\n" + 172 "Connection: close\r\n" + 173 "Content-Length: 6\r\n" + 174 "\r\n" + 175 "abcdef", 176 }, 177 178 // HTTP/1.1 POST with Content-Length in headers 179 4: { 180 Req: Request{ 181 Method: "POST", 182 URL: mustParseURL("http://example.com/"), 183 Host: "example.com", 184 Header: Header{ 185 "Content-Length": []string{"10"}, // ignored 186 }, 187 ContentLength: 6, 188 }, 189 190 Body: []byte("abcdef"), 191 192 WantWrite: "POST / HTTP/1.1\r\n" + 193 "Host: example.com\r\n" + 194 "User-Agent: Go-http-client/1.1\r\n" + 195 "Content-Length: 6\r\n" + 196 "\r\n" + 197 "abcdef", 198 199 WantProxy: "POST http://example.com/ HTTP/1.1\r\n" + 200 "Host: example.com\r\n" + 201 "User-Agent: Go-http-client/1.1\r\n" + 202 "Content-Length: 6\r\n" + 203 "\r\n" + 204 "abcdef", 205 }, 206 207 // default to HTTP/1.1 208 5: { 209 Req: Request{ 210 Method: "GET", 211 URL: mustParseURL("/search"), 212 Host: "www.google.com", 213 }, 214 215 WantWrite: "GET /search HTTP/1.1\r\n" + 216 "Host: www.google.com\r\n" + 217 "User-Agent: Go-http-client/1.1\r\n" + 218 "\r\n", 219 }, 220 221 // Request with a 0 ContentLength and a 0 byte body. 222 6: { 223 Req: Request{ 224 Method: "POST", 225 URL: mustParseURL("/"), 226 Host: "example.com", 227 ProtoMajor: 1, 228 ProtoMinor: 1, 229 ContentLength: 0, // as if unset by user 230 }, 231 232 Body: func() io.ReadCloser { return io.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) }, 233 234 WantWrite: "POST / HTTP/1.1\r\n" + 235 "Host: example.com\r\n" + 236 "User-Agent: Go-http-client/1.1\r\n" + 237 "Transfer-Encoding: chunked\r\n" + 238 "\r\n0\r\n\r\n", 239 240 WantProxy: "POST / HTTP/1.1\r\n" + 241 "Host: example.com\r\n" + 242 "User-Agent: Go-http-client/1.1\r\n" + 243 "Transfer-Encoding: chunked\r\n" + 244 "\r\n0\r\n\r\n", 245 }, 246 247 // Request with a 0 ContentLength and a nil body. 248 7: { 249 Req: Request{ 250 Method: "POST", 251 URL: mustParseURL("/"), 252 Host: "example.com", 253 ProtoMajor: 1, 254 ProtoMinor: 1, 255 ContentLength: 0, // as if unset by user 256 }, 257 258 Body: func() io.ReadCloser { return nil }, 259 260 WantWrite: "POST / HTTP/1.1\r\n" + 261 "Host: example.com\r\n" + 262 "User-Agent: Go-http-client/1.1\r\n" + 263 "Content-Length: 0\r\n" + 264 "\r\n", 265 266 WantProxy: "POST / HTTP/1.1\r\n" + 267 "Host: example.com\r\n" + 268 "User-Agent: Go-http-client/1.1\r\n" + 269 "Content-Length: 0\r\n" + 270 "\r\n", 271 }, 272 273 // Request with a 0 ContentLength and a 1 byte body. 274 8: { 275 Req: Request{ 276 Method: "POST", 277 URL: mustParseURL("/"), 278 Host: "example.com", 279 ProtoMajor: 1, 280 ProtoMinor: 1, 281 ContentLength: 0, // as if unset by user 282 }, 283 284 Body: func() io.ReadCloser { return io.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) }, 285 286 WantWrite: "POST / HTTP/1.1\r\n" + 287 "Host: example.com\r\n" + 288 "User-Agent: Go-http-client/1.1\r\n" + 289 "Transfer-Encoding: chunked\r\n\r\n" + 290 chunk("x") + chunk(""), 291 292 WantProxy: "POST / HTTP/1.1\r\n" + 293 "Host: example.com\r\n" + 294 "User-Agent: Go-http-client/1.1\r\n" + 295 "Transfer-Encoding: chunked\r\n\r\n" + 296 chunk("x") + chunk(""), 297 }, 298 299 // Request with a ContentLength of 10 but a 5 byte body. 300 9: { 301 Req: Request{ 302 Method: "POST", 303 URL: mustParseURL("/"), 304 Host: "example.com", 305 ProtoMajor: 1, 306 ProtoMinor: 1, 307 ContentLength: 10, // but we're going to send only 5 bytes 308 }, 309 Body: []byte("12345"), 310 WantError: errors.New("http: ContentLength=10 with Body length 5"), 311 }, 312 313 // Request with a ContentLength of 4 but an 8 byte body. 314 10: { 315 Req: Request{ 316 Method: "POST", 317 URL: mustParseURL("/"), 318 Host: "example.com", 319 ProtoMajor: 1, 320 ProtoMinor: 1, 321 ContentLength: 4, // but we're going to try to send 8 bytes 322 }, 323 Body: []byte("12345678"), 324 WantError: errors.New("http: ContentLength=4 with Body length 8"), 325 }, 326 327 // Request with a 5 ContentLength and nil body. 328 11: { 329 Req: Request{ 330 Method: "POST", 331 URL: mustParseURL("/"), 332 Host: "example.com", 333 ProtoMajor: 1, 334 ProtoMinor: 1, 335 ContentLength: 5, // but we'll omit the body 336 }, 337 WantError: errors.New("http: Request.ContentLength=5 with nil Body"), 338 }, 339 340 // Request with a 0 ContentLength and a body with 1 byte content and an error. 341 12: { 342 Req: Request{ 343 Method: "POST", 344 URL: mustParseURL("/"), 345 Host: "example.com", 346 ProtoMajor: 1, 347 ProtoMinor: 1, 348 ContentLength: 0, // as if unset by user 349 }, 350 351 Body: func() io.ReadCloser { 352 err := errors.New("Custom reader error") 353 errReader := iotest.ErrReader(err) 354 return io.NopCloser(io.MultiReader(strings.NewReader("x"), errReader)) 355 }, 356 357 WantError: errors.New("Custom reader error"), 358 }, 359 360 // Request with a 0 ContentLength and a body without content and an error. 361 13: { 362 Req: Request{ 363 Method: "POST", 364 URL: mustParseURL("/"), 365 Host: "example.com", 366 ProtoMajor: 1, 367 ProtoMinor: 1, 368 ContentLength: 0, // as if unset by user 369 }, 370 371 Body: func() io.ReadCloser { 372 err := errors.New("Custom reader error") 373 errReader := iotest.ErrReader(err) 374 return io.NopCloser(errReader) 375 }, 376 377 WantError: errors.New("Custom reader error"), 378 }, 379 380 // Verify that DumpRequest preserves the HTTP version number, doesn't add a Host, 381 // and doesn't add a User-Agent. 382 14: { 383 Req: Request{ 384 Method: "GET", 385 URL: mustParseURL("/foo"), 386 ProtoMajor: 1, 387 ProtoMinor: 0, 388 Header: Header{ 389 "X-Foo": []string{"X-Bar"}, 390 }, 391 }, 392 393 WantWrite: "GET /foo HTTP/1.1\r\n" + 394 "Host: \r\n" + 395 "User-Agent: Go-http-client/1.1\r\n" + 396 "X-Foo: X-Bar\r\n\r\n", 397 }, 398 399 // If no Request.Host and no Request.URL.Host, we send 400 // an empty Host header, and don't use 401 // Request.Header["Host"]. This is just testing that 402 // we don't change Go 1.0 behavior. 403 15: { 404 Req: Request{ 405 Method: "GET", 406 Host: "", 407 URL: &url.URL{ 408 Scheme: "http", 409 Host: "", 410 Path: "/search", 411 }, 412 ProtoMajor: 1, 413 ProtoMinor: 1, 414 Header: Header{ 415 "Host": []string{"bad.example.com"}, 416 }, 417 }, 418 419 WantWrite: "GET /search HTTP/1.1\r\n" + 420 "Host: \r\n" + 421 "User-Agent: Go-http-client/1.1\r\n\r\n", 422 }, 423 424 // Opaque test #1 from golang.org/issue/4860 425 16: { 426 Req: Request{ 427 Method: "GET", 428 URL: &url.URL{ 429 Scheme: "http", 430 Host: "www.google.com", 431 Opaque: "/%2F/%2F/", 432 }, 433 ProtoMajor: 1, 434 ProtoMinor: 1, 435 Header: Header{}, 436 }, 437 438 WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" + 439 "Host: www.google.com\r\n" + 440 "User-Agent: Go-http-client/1.1\r\n\r\n", 441 }, 442 443 // Opaque test #2 from golang.org/issue/4860 444 17: { 445 Req: Request{ 446 Method: "GET", 447 URL: &url.URL{ 448 Scheme: "http", 449 Host: "x.google.com", 450 Opaque: "//y.google.com/%2F/%2F/", 451 }, 452 ProtoMajor: 1, 453 ProtoMinor: 1, 454 Header: Header{}, 455 }, 456 457 WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" + 458 "Host: x.google.com\r\n" + 459 "User-Agent: Go-http-client/1.1\r\n\r\n", 460 }, 461 462 // Testing custom case in header keys. Issue 5022. 463 18: { 464 Req: Request{ 465 Method: "GET", 466 URL: &url.URL{ 467 Scheme: "http", 468 Host: "www.google.com", 469 Path: "/", 470 }, 471 Proto: "HTTP/1.1", 472 ProtoMajor: 1, 473 ProtoMinor: 1, 474 Header: Header{ 475 "ALL-CAPS": {"x"}, 476 }, 477 }, 478 479 WantWrite: "GET / HTTP/1.1\r\n" + 480 "Host: www.google.com\r\n" + 481 "User-Agent: Go-http-client/1.1\r\n" + 482 "ALL-CAPS: x\r\n" + 483 "\r\n", 484 }, 485 486 // Request with host header field; IPv6 address with zone identifier 487 19: { 488 Req: Request{ 489 Method: "GET", 490 URL: &url.URL{ 491 Host: "[fe80::1%en0]", 492 }, 493 }, 494 495 WantWrite: "GET / HTTP/1.1\r\n" + 496 "Host: [fe80::1]\r\n" + 497 "User-Agent: Go-http-client/1.1\r\n" + 498 "\r\n", 499 }, 500 501 // Request with optional host header field; IPv6 address with zone identifier 502 20: { 503 Req: Request{ 504 Method: "GET", 505 URL: &url.URL{ 506 Host: "www.example.com", 507 }, 508 Host: "[fe80::1%en0]:8080", 509 }, 510 511 WantWrite: "GET / HTTP/1.1\r\n" + 512 "Host: [fe80::1]:8080\r\n" + 513 "User-Agent: Go-http-client/1.1\r\n" + 514 "\r\n", 515 }, 516 517 // CONNECT without Opaque 518 21: { 519 Req: Request{ 520 Method: "CONNECT", 521 URL: &url.URL{ 522 Scheme: "https", // of proxy.com 523 Host: "proxy.com", 524 }, 525 }, 526 // What we used to do, locking that behavior in: 527 WantWrite: "CONNECT proxy.com HTTP/1.1\r\n" + 528 "Host: proxy.com\r\n" + 529 "User-Agent: Go-http-client/1.1\r\n" + 530 "\r\n", 531 }, 532 533 // CONNECT with Opaque 534 22: { 535 Req: Request{ 536 Method: "CONNECT", 537 URL: &url.URL{ 538 Scheme: "https", // of proxy.com 539 Host: "proxy.com", 540 Opaque: "backend:443", 541 }, 542 }, 543 WantWrite: "CONNECT backend:443 HTTP/1.1\r\n" + 544 "Host: proxy.com\r\n" + 545 "User-Agent: Go-http-client/1.1\r\n" + 546 "\r\n", 547 }, 548 549 // Verify that a nil header value doesn't get written. 550 23: { 551 Req: Request{ 552 Method: "GET", 553 URL: mustParseURL("/foo"), 554 Header: Header{ 555 "X-Foo": []string{"X-Bar"}, 556 "X-Idempotency-Key": nil, 557 }, 558 }, 559 560 WantWrite: "GET /foo HTTP/1.1\r\n" + 561 "Host: \r\n" + 562 "User-Agent: Go-http-client/1.1\r\n" + 563 "X-Foo: X-Bar\r\n\r\n", 564 }, 565 24: { 566 Req: Request{ 567 Method: "GET", 568 URL: mustParseURL("/foo"), 569 Header: Header{ 570 "X-Foo": []string{"X-Bar"}, 571 "X-Idempotency-Key": []string{}, 572 }, 573 }, 574 575 WantWrite: "GET /foo HTTP/1.1\r\n" + 576 "Host: \r\n" + 577 "User-Agent: Go-http-client/1.1\r\n" + 578 "X-Foo: X-Bar\r\n\r\n", 579 }, 580 581 25: { 582 Req: Request{ 583 Method: "GET", 584 URL: &url.URL{ 585 Host: "www.example.com", 586 RawQuery: "new\nline", // or any CTL 587 }, 588 }, 589 WantError: errors.New("gitee.com/ks-custle/core-gm/gmhttp: can't write control character in Request.URL"), 590 }, 591 592 26: { // Request with nil body and PATCH method. Issue #40978 593 Req: Request{ 594 Method: "PATCH", 595 URL: mustParseURL("/"), 596 Host: "example.com", 597 ProtoMajor: 1, 598 ProtoMinor: 1, 599 ContentLength: 0, // as if unset by user 600 }, 601 Body: nil, 602 WantWrite: "PATCH / HTTP/1.1\r\n" + 603 "Host: example.com\r\n" + 604 "User-Agent: Go-http-client/1.1\r\n" + 605 "Content-Length: 0\r\n\r\n", 606 WantProxy: "PATCH / HTTP/1.1\r\n" + 607 "Host: example.com\r\n" + 608 "User-Agent: Go-http-client/1.1\r\n" + 609 "Content-Length: 0\r\n\r\n", 610 }, 611 } 612 613 func TestRequestWrite(t *testing.T) { 614 for i := range reqWriteTests { 615 tt := &reqWriteTests[i] 616 617 setBody := func() { 618 if tt.Body == nil { 619 return 620 } 621 switch b := tt.Body.(type) { 622 case []byte: 623 tt.Req.Body = io.NopCloser(bytes.NewReader(b)) 624 case func() io.ReadCloser: 625 tt.Req.Body = b() 626 } 627 } 628 setBody() 629 if tt.Req.Header == nil { 630 tt.Req.Header = make(Header) 631 } 632 633 var braw bytes.Buffer 634 err := tt.Req.Write(&braw) 635 if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e { 636 t.Errorf("writing #%d, err = %q, want %q", i, g, e) 637 continue 638 } 639 if err != nil { 640 continue 641 } 642 643 if tt.WantWrite != "" { 644 sraw := braw.String() 645 if sraw != tt.WantWrite { 646 t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw) 647 continue 648 } 649 } 650 651 if tt.WantProxy != "" { 652 setBody() 653 var praw bytes.Buffer 654 err = tt.Req.WriteProxy(&praw) 655 if err != nil { 656 t.Errorf("WriteProxy #%d: %s", i, err) 657 continue 658 } 659 sraw := praw.String() 660 if sraw != tt.WantProxy { 661 t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw) 662 continue 663 } 664 } 665 } 666 } 667 668 func TestRequestWriteTransport(t *testing.T) { 669 t.Parallel() 670 671 matchSubstr := func(substr string) func(string) error { 672 return func(written string) error { 673 if !strings.Contains(written, substr) { 674 return fmt.Errorf("expected substring %q in request: %s", substr, written) 675 } 676 return nil 677 } 678 } 679 680 noContentLengthOrTransferEncoding := func(req string) error { 681 if strings.Contains(req, "Content-Length: ") { 682 return fmt.Errorf("unexpected Content-Length in request: %s", req) 683 } 684 if strings.Contains(req, "Transfer-Encoding: ") { 685 return fmt.Errorf("unexpected Transfer-Encoding in request: %s", req) 686 } 687 return nil 688 } 689 690 all := func(checks ...func(string) error) func(string) error { 691 return func(req string) error { 692 for _, c := range checks { 693 if err := c(req); err != nil { 694 return err 695 } 696 } 697 return nil 698 } 699 } 700 701 type testCase struct { 702 method string 703 clen int64 // ContentLength 704 body io.ReadCloser 705 want func(string) error 706 707 // optional: 708 init func(*testCase) 709 afterReqRead func() 710 } 711 712 tests := []testCase{ 713 { 714 method: "GET", 715 want: noContentLengthOrTransferEncoding, 716 }, 717 { 718 method: "GET", 719 body: io.NopCloser(strings.NewReader("")), 720 want: noContentLengthOrTransferEncoding, 721 }, 722 { 723 method: "GET", 724 clen: -1, 725 body: io.NopCloser(strings.NewReader("")), 726 want: noContentLengthOrTransferEncoding, 727 }, 728 // A GET with a body, with explicit content length: 729 { 730 method: "GET", 731 clen: 7, 732 body: io.NopCloser(strings.NewReader("foobody")), 733 want: all(matchSubstr("Content-Length: 7"), 734 matchSubstr("foobody")), 735 }, 736 // A GET with a body, sniffing the leading "f" from "foobody". 737 { 738 method: "GET", 739 clen: -1, 740 body: io.NopCloser(strings.NewReader("foobody")), 741 want: all(matchSubstr("Transfer-Encoding: chunked"), 742 matchSubstr("\r\n1\r\nf\r\n"), 743 matchSubstr("oobody")), 744 }, 745 // But a POST request is expected to have a body, so 746 // no sniffing happens: 747 { 748 method: "POST", 749 clen: -1, 750 body: io.NopCloser(strings.NewReader("foobody")), 751 want: all(matchSubstr("Transfer-Encoding: chunked"), 752 matchSubstr("foobody")), 753 }, 754 { 755 method: "POST", 756 clen: -1, 757 body: io.NopCloser(strings.NewReader("")), 758 want: all(matchSubstr("Transfer-Encoding: chunked")), 759 }, 760 // Verify that a blocking Request.Body doesn't block forever. 761 { 762 method: "GET", 763 clen: -1, 764 init: func(tt *testCase) { 765 pr, pw := io.Pipe() 766 tt.afterReqRead = func() { 767 _ = pw.Close() 768 } 769 tt.body = io.NopCloser(pr) 770 }, 771 want: matchSubstr("Transfer-Encoding: chunked"), 772 }, 773 } 774 775 for i, tt := range tests { 776 if tt.init != nil { 777 tt.init(&tt) 778 } 779 req := &Request{ 780 Method: tt.method, 781 URL: &url.URL{ 782 Scheme: "http", 783 Host: "example.com", 784 }, 785 Header: make(Header), 786 ContentLength: tt.clen, 787 Body: tt.body, 788 } 789 got, err := dumpRequestOut(req, tt.afterReqRead) 790 if err != nil { 791 t.Errorf("test[%d]: %v", i, err) 792 continue 793 } 794 if err := tt.want(string(got)); err != nil { 795 t.Errorf("test[%d]: %v", i, err) 796 } 797 } 798 } 799 800 type closeChecker struct { 801 io.Reader 802 closed bool 803 } 804 805 func (rc *closeChecker) Close() error { 806 rc.closed = true 807 return nil 808 } 809 810 // TestRequestWriteClosesBody tests that Request.Write closes its request.Body. 811 // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer 812 // inside a NopCloser, and that it serializes it correctly. 813 func TestRequestWriteClosesBody(t *testing.T) { 814 rc := &closeChecker{Reader: strings.NewReader("my body")} 815 //goland:noinspection HttpUrlsUsage 816 req, err := NewRequest("POST", "http://foo.com/", rc) 817 if err != nil { 818 t.Fatal(err) 819 } 820 buf := new(bytes.Buffer) 821 if err := req.Write(buf); err != nil { 822 t.Error(err) 823 } 824 if !rc.closed { 825 t.Error("body not closed after write") 826 } 827 expected := "POST / HTTP/1.1\r\n" + 828 "Host: foo.com\r\n" + 829 "User-Agent: Go-http-client/1.1\r\n" + 830 "Transfer-Encoding: chunked\r\n\r\n" + 831 chunk("my body") + 832 chunk("") 833 if buf.String() != expected { 834 t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected) 835 } 836 } 837 838 func chunk(s string) string { 839 return fmt.Sprintf("%x\r\n%s\r\n", len(s), s) 840 } 841 842 func mustParseURL(s string) *url.URL { 843 u, err := url.Parse(s) 844 if err != nil { 845 panic(fmt.Sprintf("Error parsing URL %q: %v", s, err)) 846 } 847 return u 848 } 849 850 type writerFunc func([]byte) (int, error) 851 852 func (f writerFunc) Write(p []byte) (int, error) { return f(p) } 853 854 // TestRequestWriteError tests the Write err != nil checks in (*Request).write. 855 func TestRequestWriteError(t *testing.T) { 856 failAfter, writeCount := 0, 0 857 errFail := errors.New("fake write failure") 858 859 // w is the buffered io.Writer to write the request to. It 860 // fails exactly once on its Nth Write call, as controlled by 861 // failAfter. It also tracks the number of calls in 862 // writeCount. 863 w := struct { 864 io.ByteWriter // to avoid being wrapped by a bufio.Writer 865 io.Writer 866 }{ 867 nil, 868 writerFunc(func(p []byte) (n int, err error) { 869 writeCount++ 870 if failAfter == 0 { 871 err = errFail 872 } 873 failAfter-- 874 return len(p), err 875 }), 876 } 877 878 //goland:noinspection HttpUrlsUsage 879 req, _ := NewRequest("GET", "http://example.com/", nil) 880 const writeCalls = 4 // number of Write calls in current implementation 881 sawGood := false 882 for n := 0; n <= writeCalls+2; n++ { 883 failAfter = n 884 writeCount = 0 885 err := req.Write(w) 886 var wantErr error 887 if n < writeCalls { 888 wantErr = errFail 889 } 890 if err != wantErr { 891 t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr) 892 continue 893 } 894 if err == nil { 895 sawGood = true 896 if writeCount != writeCalls { 897 t.Fatalf("writeCalls constant is outdated in test") 898 } 899 } 900 if writeCount > writeCalls || writeCount > n+1 { 901 t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount) 902 } 903 } 904 if !sawGood { 905 t.Fatalf("writeCalls constant is outdated in test") 906 } 907 } 908 909 // dumpRequestOut is a modified copy of net/http/httputil.DumpRequestOut. 910 // Unlike the original, this version doesn't mutate the req.Body and 911 // try to restore it. It always dumps the whole body. 912 // And it doesn't support https. 913 func dumpRequestOut(req *Request, onReadHeaders func()) ([]byte, error) { 914 915 // Use the actual Transport code to record what we would send 916 // on the wire, but not using TCP. Use a Transport with a 917 // custom dialer that returns a fake net.Conn that waits 918 // for the full input (and recording it), and then responds 919 // with a dummy response. 920 var buf bytes.Buffer // records the output 921 pr, pw := io.Pipe() 922 defer func(pr *io.PipeReader) { 923 _ = pr.Close() 924 }(pr) 925 defer func(pw *io.PipeWriter) { 926 _ = pw.Close() 927 }(pw) 928 dr := &delegateReader{c: make(chan io.Reader)} 929 930 t := &Transport{ 931 Dial: func(net, addr string) (net.Conn, error) { 932 return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil 933 }, 934 } 935 defer t.CloseIdleConnections() 936 937 // Wait for the request before replying with a dummy response: 938 go func() { 939 req, err := ReadRequest(bufio.NewReader(pr)) 940 if err == nil { 941 if onReadHeaders != nil { 942 onReadHeaders() 943 } 944 // Ensure all the body is read; otherwise 945 // we'll get a partial dump. 946 _, _ = io.Copy(io.Discard, req.Body) 947 _ = req.Body.Close() 948 } 949 dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n") 950 }() 951 952 _, err := t.RoundTrip(req) 953 if err != nil { 954 return nil, err 955 } 956 return buf.Bytes(), nil 957 } 958 959 // delegateReader is a reader that delegates to another reader, 960 // once it arrives on a channel. 961 type delegateReader struct { 962 c chan io.Reader 963 r io.Reader // nil until received from c 964 } 965 966 func (r *delegateReader) Read(p []byte) (int, error) { 967 if r.r == nil { 968 r.r = <-r.c 969 } 970 return r.r.Read(p) 971 } 972 973 // dumpConn is a net.Conn that writes to Writer and reads from Reader. 974 type dumpConn struct { 975 io.Writer 976 io.Reader 977 } 978 979 func (c *dumpConn) Close() error { return nil } 980 func (c *dumpConn) LocalAddr() net.Addr { return nil } 981 func (c *dumpConn) RemoteAddr() net.Addr { return nil } 982 983 //goland:noinspection GoUnusedParameter 984 func (c *dumpConn) SetDeadline(t time.Time) error { return nil } 985 986 //goland:noinspection GoUnusedParameter 987 func (c *dumpConn) SetReadDeadline(t time.Time) error { return nil } 988 989 //goland:noinspection GoUnusedParameter 990 func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil }