github.com/dannin/go@v0.0.0-20161031215817-d35dfd405eaa/src/net/http/requestwrite_test.go (about) 1 // Copyright 2010 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package http 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "net/url" 14 "strings" 15 "testing" 16 ) 17 18 type reqWriteTest struct { 19 Req Request 20 Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body 21 22 // Any of these three may be empty to skip that test. 23 WantWrite string // Request.Write 24 WantProxy string // Request.WriteProxy 25 26 WantError error // wanted error from Request.Write 27 } 28 29 var reqWriteTests = []reqWriteTest{ 30 // HTTP/1.1 => chunked coding; no body; no trailer 31 0: { 32 Req: Request{ 33 Method: "GET", 34 URL: &url.URL{ 35 Scheme: "http", 36 Host: "www.techcrunch.com", 37 Path: "/", 38 }, 39 Proto: "HTTP/1.1", 40 ProtoMajor: 1, 41 ProtoMinor: 1, 42 Header: Header{ 43 "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, 44 "Accept-Charset": {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"}, 45 "Accept-Encoding": {"gzip,deflate"}, 46 "Accept-Language": {"en-us,en;q=0.5"}, 47 "Keep-Alive": {"300"}, 48 "Proxy-Connection": {"keep-alive"}, 49 "User-Agent": {"Fake"}, 50 }, 51 Body: nil, 52 Close: false, 53 Host: "www.techcrunch.com", 54 Form: map[string][]string{}, 55 }, 56 57 WantWrite: "GET / HTTP/1.1\r\n" + 58 "Host: www.techcrunch.com\r\n" + 59 "User-Agent: Fake\r\n" + 60 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + 61 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + 62 "Accept-Encoding: gzip,deflate\r\n" + 63 "Accept-Language: en-us,en;q=0.5\r\n" + 64 "Keep-Alive: 300\r\n" + 65 "Proxy-Connection: keep-alive\r\n\r\n", 66 67 WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" + 68 "Host: www.techcrunch.com\r\n" + 69 "User-Agent: Fake\r\n" + 70 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + 71 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + 72 "Accept-Encoding: gzip,deflate\r\n" + 73 "Accept-Language: en-us,en;q=0.5\r\n" + 74 "Keep-Alive: 300\r\n" + 75 "Proxy-Connection: keep-alive\r\n\r\n", 76 }, 77 // HTTP/1.1 => chunked coding; body; empty trailer 78 1: { 79 Req: Request{ 80 Method: "GET", 81 URL: &url.URL{ 82 Scheme: "http", 83 Host: "www.google.com", 84 Path: "/search", 85 }, 86 ProtoMajor: 1, 87 ProtoMinor: 1, 88 Header: Header{}, 89 TransferEncoding: []string{"chunked"}, 90 }, 91 92 Body: []byte("abcdef"), 93 94 WantWrite: "GET /search HTTP/1.1\r\n" + 95 "Host: www.google.com\r\n" + 96 "User-Agent: Go-http-client/1.1\r\n" + 97 "Transfer-Encoding: chunked\r\n\r\n" + 98 chunk("abcdef") + chunk(""), 99 100 WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" + 101 "Host: www.google.com\r\n" + 102 "User-Agent: Go-http-client/1.1\r\n" + 103 "Transfer-Encoding: chunked\r\n\r\n" + 104 chunk("abcdef") + chunk(""), 105 }, 106 // HTTP/1.1 POST => chunked coding; body; empty trailer 107 2: { 108 Req: Request{ 109 Method: "POST", 110 URL: &url.URL{ 111 Scheme: "http", 112 Host: "www.google.com", 113 Path: "/search", 114 }, 115 ProtoMajor: 1, 116 ProtoMinor: 1, 117 Header: Header{}, 118 Close: true, 119 TransferEncoding: []string{"chunked"}, 120 }, 121 122 Body: []byte("abcdef"), 123 124 WantWrite: "POST /search HTTP/1.1\r\n" + 125 "Host: www.google.com\r\n" + 126 "User-Agent: Go-http-client/1.1\r\n" + 127 "Connection: close\r\n" + 128 "Transfer-Encoding: chunked\r\n\r\n" + 129 chunk("abcdef") + chunk(""), 130 131 WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" + 132 "Host: www.google.com\r\n" + 133 "User-Agent: Go-http-client/1.1\r\n" + 134 "Connection: close\r\n" + 135 "Transfer-Encoding: chunked\r\n\r\n" + 136 chunk("abcdef") + chunk(""), 137 }, 138 139 // HTTP/1.1 POST with Content-Length, no chunking 140 3: { 141 Req: Request{ 142 Method: "POST", 143 URL: &url.URL{ 144 Scheme: "http", 145 Host: "www.google.com", 146 Path: "/search", 147 }, 148 ProtoMajor: 1, 149 ProtoMinor: 1, 150 Header: Header{}, 151 Close: true, 152 ContentLength: 6, 153 }, 154 155 Body: []byte("abcdef"), 156 157 WantWrite: "POST /search HTTP/1.1\r\n" + 158 "Host: www.google.com\r\n" + 159 "User-Agent: Go-http-client/1.1\r\n" + 160 "Connection: close\r\n" + 161 "Content-Length: 6\r\n" + 162 "\r\n" + 163 "abcdef", 164 165 WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" + 166 "Host: www.google.com\r\n" + 167 "User-Agent: Go-http-client/1.1\r\n" + 168 "Connection: close\r\n" + 169 "Content-Length: 6\r\n" + 170 "\r\n" + 171 "abcdef", 172 }, 173 174 // HTTP/1.1 POST with Content-Length in headers 175 4: { 176 Req: Request{ 177 Method: "POST", 178 URL: mustParseURL("http://example.com/"), 179 Host: "example.com", 180 Header: Header{ 181 "Content-Length": []string{"10"}, // ignored 182 }, 183 ContentLength: 6, 184 }, 185 186 Body: []byte("abcdef"), 187 188 WantWrite: "POST / HTTP/1.1\r\n" + 189 "Host: example.com\r\n" + 190 "User-Agent: Go-http-client/1.1\r\n" + 191 "Content-Length: 6\r\n" + 192 "\r\n" + 193 "abcdef", 194 195 WantProxy: "POST http://example.com/ HTTP/1.1\r\n" + 196 "Host: example.com\r\n" + 197 "User-Agent: Go-http-client/1.1\r\n" + 198 "Content-Length: 6\r\n" + 199 "\r\n" + 200 "abcdef", 201 }, 202 203 // default to HTTP/1.1 204 5: { 205 Req: Request{ 206 Method: "GET", 207 URL: mustParseURL("/search"), 208 Host: "www.google.com", 209 }, 210 211 WantWrite: "GET /search HTTP/1.1\r\n" + 212 "Host: www.google.com\r\n" + 213 "User-Agent: Go-http-client/1.1\r\n" + 214 "\r\n", 215 }, 216 217 // Request with a 0 ContentLength and a 0 byte body. 218 6: { 219 Req: Request{ 220 Method: "POST", 221 URL: mustParseURL("/"), 222 Host: "example.com", 223 ProtoMajor: 1, 224 ProtoMinor: 1, 225 ContentLength: 0, // as if unset by user 226 }, 227 228 Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) }, 229 230 WantWrite: "POST / HTTP/1.1\r\n" + 231 "Host: example.com\r\n" + 232 "User-Agent: Go-http-client/1.1\r\n" + 233 "Transfer-Encoding: chunked\r\n" + 234 "\r\n0\r\n\r\n", 235 236 WantProxy: "POST / HTTP/1.1\r\n" + 237 "Host: example.com\r\n" + 238 "User-Agent: Go-http-client/1.1\r\n" + 239 "Transfer-Encoding: chunked\r\n" + 240 "\r\n0\r\n\r\n", 241 }, 242 243 // Request with a 0 ContentLength and a nil body. 244 7: { 245 Req: Request{ 246 Method: "POST", 247 URL: mustParseURL("/"), 248 Host: "example.com", 249 ProtoMajor: 1, 250 ProtoMinor: 1, 251 ContentLength: 0, // as if unset by user 252 }, 253 254 Body: func() io.ReadCloser { return nil }, 255 256 WantWrite: "POST / HTTP/1.1\r\n" + 257 "Host: example.com\r\n" + 258 "User-Agent: Go-http-client/1.1\r\n" + 259 "Content-Length: 0\r\n" + 260 "\r\n", 261 262 WantProxy: "POST / HTTP/1.1\r\n" + 263 "Host: example.com\r\n" + 264 "User-Agent: Go-http-client/1.1\r\n" + 265 "Content-Length: 0\r\n" + 266 "\r\n", 267 }, 268 269 // Request with a 0 ContentLength and a 1 byte body. 270 8: { 271 Req: Request{ 272 Method: "POST", 273 URL: mustParseURL("/"), 274 Host: "example.com", 275 ProtoMajor: 1, 276 ProtoMinor: 1, 277 ContentLength: 0, // as if unset by user 278 }, 279 280 Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) }, 281 282 WantWrite: "POST / HTTP/1.1\r\n" + 283 "Host: example.com\r\n" + 284 "User-Agent: Go-http-client/1.1\r\n" + 285 "Transfer-Encoding: chunked\r\n\r\n" + 286 chunk("x") + chunk(""), 287 288 WantProxy: "POST / HTTP/1.1\r\n" + 289 "Host: example.com\r\n" + 290 "User-Agent: Go-http-client/1.1\r\n" + 291 "Transfer-Encoding: chunked\r\n\r\n" + 292 chunk("x") + chunk(""), 293 }, 294 295 // Request with a ContentLength of 10 but a 5 byte body. 296 9: { 297 Req: Request{ 298 Method: "POST", 299 URL: mustParseURL("/"), 300 Host: "example.com", 301 ProtoMajor: 1, 302 ProtoMinor: 1, 303 ContentLength: 10, // but we're going to send only 5 bytes 304 }, 305 Body: []byte("12345"), 306 WantError: errors.New("http: ContentLength=10 with Body length 5"), 307 }, 308 309 // Request with a ContentLength of 4 but an 8 byte body. 310 10: { 311 Req: Request{ 312 Method: "POST", 313 URL: mustParseURL("/"), 314 Host: "example.com", 315 ProtoMajor: 1, 316 ProtoMinor: 1, 317 ContentLength: 4, // but we're going to try to send 8 bytes 318 }, 319 Body: []byte("12345678"), 320 WantError: errors.New("http: ContentLength=4 with Body length 8"), 321 }, 322 323 // Request with a 5 ContentLength and nil body. 324 11: { 325 Req: Request{ 326 Method: "POST", 327 URL: mustParseURL("/"), 328 Host: "example.com", 329 ProtoMajor: 1, 330 ProtoMinor: 1, 331 ContentLength: 5, // but we'll omit the body 332 }, 333 WantError: errors.New("http: Request.ContentLength=5 with nil Body"), 334 }, 335 336 // Request with a 0 ContentLength and a body with 1 byte content and an error. 337 12: { 338 Req: Request{ 339 Method: "POST", 340 URL: mustParseURL("/"), 341 Host: "example.com", 342 ProtoMajor: 1, 343 ProtoMinor: 1, 344 ContentLength: 0, // as if unset by user 345 }, 346 347 Body: func() io.ReadCloser { 348 err := errors.New("Custom reader error") 349 errReader := &errorReader{err} 350 return ioutil.NopCloser(io.MultiReader(strings.NewReader("x"), errReader)) 351 }, 352 353 WantError: errors.New("Custom reader error"), 354 }, 355 356 // Request with a 0 ContentLength and a body without content and an error. 357 13: { 358 Req: Request{ 359 Method: "POST", 360 URL: mustParseURL("/"), 361 Host: "example.com", 362 ProtoMajor: 1, 363 ProtoMinor: 1, 364 ContentLength: 0, // as if unset by user 365 }, 366 367 Body: func() io.ReadCloser { 368 err := errors.New("Custom reader error") 369 errReader := &errorReader{err} 370 return ioutil.NopCloser(errReader) 371 }, 372 373 WantError: errors.New("Custom reader error"), 374 }, 375 376 // Verify that DumpRequest preserves the HTTP version number, doesn't add a Host, 377 // and doesn't add a User-Agent. 378 14: { 379 Req: Request{ 380 Method: "GET", 381 URL: mustParseURL("/foo"), 382 ProtoMajor: 1, 383 ProtoMinor: 0, 384 Header: Header{ 385 "X-Foo": []string{"X-Bar"}, 386 }, 387 }, 388 389 WantWrite: "GET /foo HTTP/1.1\r\n" + 390 "Host: \r\n" + 391 "User-Agent: Go-http-client/1.1\r\n" + 392 "X-Foo: X-Bar\r\n\r\n", 393 }, 394 395 // If no Request.Host and no Request.URL.Host, we send 396 // an empty Host header, and don't use 397 // Request.Header["Host"]. This is just testing that 398 // we don't change Go 1.0 behavior. 399 15: { 400 Req: Request{ 401 Method: "GET", 402 Host: "", 403 URL: &url.URL{ 404 Scheme: "http", 405 Host: "", 406 Path: "/search", 407 }, 408 ProtoMajor: 1, 409 ProtoMinor: 1, 410 Header: Header{ 411 "Host": []string{"bad.example.com"}, 412 }, 413 }, 414 415 WantWrite: "GET /search HTTP/1.1\r\n" + 416 "Host: \r\n" + 417 "User-Agent: Go-http-client/1.1\r\n\r\n", 418 }, 419 420 // Opaque test #1 from golang.org/issue/4860 421 16: { 422 Req: Request{ 423 Method: "GET", 424 URL: &url.URL{ 425 Scheme: "http", 426 Host: "www.google.com", 427 Opaque: "/%2F/%2F/", 428 }, 429 ProtoMajor: 1, 430 ProtoMinor: 1, 431 Header: Header{}, 432 }, 433 434 WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" + 435 "Host: www.google.com\r\n" + 436 "User-Agent: Go-http-client/1.1\r\n\r\n", 437 }, 438 439 // Opaque test #2 from golang.org/issue/4860 440 17: { 441 Req: Request{ 442 Method: "GET", 443 URL: &url.URL{ 444 Scheme: "http", 445 Host: "x.google.com", 446 Opaque: "//y.google.com/%2F/%2F/", 447 }, 448 ProtoMajor: 1, 449 ProtoMinor: 1, 450 Header: Header{}, 451 }, 452 453 WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" + 454 "Host: x.google.com\r\n" + 455 "User-Agent: Go-http-client/1.1\r\n\r\n", 456 }, 457 458 // Testing custom case in header keys. Issue 5022. 459 18: { 460 Req: Request{ 461 Method: "GET", 462 URL: &url.URL{ 463 Scheme: "http", 464 Host: "www.google.com", 465 Path: "/", 466 }, 467 Proto: "HTTP/1.1", 468 ProtoMajor: 1, 469 ProtoMinor: 1, 470 Header: Header{ 471 "ALL-CAPS": {"x"}, 472 }, 473 }, 474 475 WantWrite: "GET / HTTP/1.1\r\n" + 476 "Host: www.google.com\r\n" + 477 "User-Agent: Go-http-client/1.1\r\n" + 478 "ALL-CAPS: x\r\n" + 479 "\r\n", 480 }, 481 482 // Request with host header field; IPv6 address with zone identifier 483 19: { 484 Req: Request{ 485 Method: "GET", 486 URL: &url.URL{ 487 Host: "[fe80::1%en0]", 488 }, 489 }, 490 491 WantWrite: "GET / HTTP/1.1\r\n" + 492 "Host: [fe80::1]\r\n" + 493 "User-Agent: Go-http-client/1.1\r\n" + 494 "\r\n", 495 }, 496 497 // Request with optional host header field; IPv6 address with zone identifier 498 20: { 499 Req: Request{ 500 Method: "GET", 501 URL: &url.URL{ 502 Host: "www.example.com", 503 }, 504 Host: "[fe80::1%en0]:8080", 505 }, 506 507 WantWrite: "GET / HTTP/1.1\r\n" + 508 "Host: [fe80::1]:8080\r\n" + 509 "User-Agent: Go-http-client/1.1\r\n" + 510 "\r\n", 511 }, 512 } 513 514 func TestRequestWrite(t *testing.T) { 515 for i := range reqWriteTests { 516 tt := &reqWriteTests[i] 517 518 setBody := func() { 519 if tt.Body == nil { 520 return 521 } 522 switch b := tt.Body.(type) { 523 case []byte: 524 tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b)) 525 case func() io.ReadCloser: 526 tt.Req.Body = b() 527 } 528 } 529 setBody() 530 if tt.Req.Header == nil { 531 tt.Req.Header = make(Header) 532 } 533 534 var braw bytes.Buffer 535 err := tt.Req.Write(&braw) 536 if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e { 537 t.Errorf("writing #%d, err = %q, want %q", i, g, e) 538 continue 539 } 540 if err != nil { 541 continue 542 } 543 544 if tt.WantWrite != "" { 545 sraw := braw.String() 546 if sraw != tt.WantWrite { 547 t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw) 548 continue 549 } 550 } 551 552 if tt.WantProxy != "" { 553 setBody() 554 var praw bytes.Buffer 555 err = tt.Req.WriteProxy(&praw) 556 if err != nil { 557 t.Errorf("WriteProxy #%d: %s", i, err) 558 continue 559 } 560 sraw := praw.String() 561 if sraw != tt.WantProxy { 562 t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw) 563 continue 564 } 565 } 566 } 567 } 568 569 type closeChecker struct { 570 io.Reader 571 closed bool 572 } 573 574 func (rc *closeChecker) Close() error { 575 rc.closed = true 576 return nil 577 } 578 579 // TestRequestWriteClosesBody tests that Request.Write closes its request.Body. 580 // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer 581 // inside a NopCloser, and that it serializes it correctly. 582 func TestRequestWriteClosesBody(t *testing.T) { 583 rc := &closeChecker{Reader: strings.NewReader("my body")} 584 req, _ := NewRequest("POST", "http://foo.com/", rc) 585 if req.ContentLength != -1 { 586 t.Errorf("got req.ContentLength %d, want -1", req.ContentLength) 587 } 588 buf := new(bytes.Buffer) 589 req.Write(buf) 590 if !rc.closed { 591 t.Error("body not closed after write") 592 } 593 expected := "POST / HTTP/1.1\r\n" + 594 "Host: foo.com\r\n" + 595 "User-Agent: Go-http-client/1.1\r\n" + 596 "Transfer-Encoding: chunked\r\n\r\n" + 597 chunk("my body") + 598 chunk("") 599 if buf.String() != expected { 600 t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected) 601 } 602 } 603 604 func chunk(s string) string { 605 return fmt.Sprintf("%x\r\n%s\r\n", len(s), s) 606 } 607 608 func mustParseURL(s string) *url.URL { 609 u, err := url.Parse(s) 610 if err != nil { 611 panic(fmt.Sprintf("Error parsing URL %q: %v", s, err)) 612 } 613 return u 614 } 615 616 type writerFunc func([]byte) (int, error) 617 618 func (f writerFunc) Write(p []byte) (int, error) { return f(p) } 619 620 // TestRequestWriteError tests the Write err != nil checks in (*Request).write. 621 func TestRequestWriteError(t *testing.T) { 622 failAfter, writeCount := 0, 0 623 errFail := errors.New("fake write failure") 624 625 // w is the buffered io.Writer to write the request to. It 626 // fails exactly once on its Nth Write call, as controlled by 627 // failAfter. It also tracks the number of calls in 628 // writeCount. 629 w := struct { 630 io.ByteWriter // to avoid being wrapped by a bufio.Writer 631 io.Writer 632 }{ 633 nil, 634 writerFunc(func(p []byte) (n int, err error) { 635 writeCount++ 636 if failAfter == 0 { 637 err = errFail 638 } 639 failAfter-- 640 return len(p), err 641 }), 642 } 643 644 req, _ := NewRequest("GET", "http://example.com/", nil) 645 const writeCalls = 4 // number of Write calls in current implementation 646 sawGood := false 647 for n := 0; n <= writeCalls+2; n++ { 648 failAfter = n 649 writeCount = 0 650 err := req.Write(w) 651 var wantErr error 652 if n < writeCalls { 653 wantErr = errFail 654 } 655 if err != wantErr { 656 t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr) 657 continue 658 } 659 if err == nil { 660 sawGood = true 661 if writeCount != writeCalls { 662 t.Fatalf("writeCalls constant is outdated in test") 663 } 664 } 665 if writeCount > writeCalls || writeCount > n+1 { 666 t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount) 667 } 668 } 669 if !sawGood { 670 t.Fatalf("writeCalls constant is outdated in test") 671 } 672 }