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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 // RFC 2616 Section 14.13 says Content-Length should be specified 231 // unless body is prohibited by the request method. 232 // Also, nginx expects it for POST and PUT. 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 "Content-Length: 0\r\n" + 237 "\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 "Content-Length: 0\r\n" + 243 "\r\n", 244 }, 245 246 // Request with a 0 ContentLength and a 1 byte body. 247 { 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 ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) }, 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 "Transfer-Encoding: chunked\r\n\r\n" + 263 chunk("x") + chunk(""), 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 "Transfer-Encoding: chunked\r\n\r\n" + 269 chunk("x") + chunk(""), 270 }, 271 272 // Request with a ContentLength of 10 but a 5 byte body. 273 { 274 Req: Request{ 275 Method: "POST", 276 URL: mustParseURL("/"), 277 Host: "example.com", 278 ProtoMajor: 1, 279 ProtoMinor: 1, 280 ContentLength: 10, // but we're going to send only 5 bytes 281 }, 282 Body: []byte("12345"), 283 WantError: errors.New("http: ContentLength=10 with Body length 5"), 284 }, 285 286 // Request with a ContentLength of 4 but an 8 byte body. 287 { 288 Req: Request{ 289 Method: "POST", 290 URL: mustParseURL("/"), 291 Host: "example.com", 292 ProtoMajor: 1, 293 ProtoMinor: 1, 294 ContentLength: 4, // but we're going to try to send 8 bytes 295 }, 296 Body: []byte("12345678"), 297 WantError: errors.New("http: ContentLength=4 with Body length 8"), 298 }, 299 300 // Request with a 5 ContentLength and nil body. 301 { 302 Req: Request{ 303 Method: "POST", 304 URL: mustParseURL("/"), 305 Host: "example.com", 306 ProtoMajor: 1, 307 ProtoMinor: 1, 308 ContentLength: 5, // but we'll omit the body 309 }, 310 WantError: errors.New("http: Request.ContentLength=5 with nil Body"), 311 }, 312 313 // Request with a 0 ContentLength and a body with 1 byte content and an error. 314 { 315 Req: Request{ 316 Method: "POST", 317 URL: mustParseURL("/"), 318 Host: "example.com", 319 ProtoMajor: 1, 320 ProtoMinor: 1, 321 ContentLength: 0, // as if unset by user 322 }, 323 324 Body: func() io.ReadCloser { 325 err := errors.New("Custom reader error") 326 errReader := &errorReader{err} 327 return ioutil.NopCloser(io.MultiReader(strings.NewReader("x"), errReader)) 328 }, 329 330 WantError: errors.New("Custom reader error"), 331 }, 332 333 // Request with a 0 ContentLength and a body without content and an error. 334 { 335 Req: Request{ 336 Method: "POST", 337 URL: mustParseURL("/"), 338 Host: "example.com", 339 ProtoMajor: 1, 340 ProtoMinor: 1, 341 ContentLength: 0, // as if unset by user 342 }, 343 344 Body: func() io.ReadCloser { 345 err := errors.New("Custom reader error") 346 errReader := &errorReader{err} 347 return ioutil.NopCloser(errReader) 348 }, 349 350 WantError: errors.New("Custom reader error"), 351 }, 352 353 // Verify that DumpRequest preserves the HTTP version number, doesn't add a Host, 354 // and doesn't add a User-Agent. 355 { 356 Req: Request{ 357 Method: "GET", 358 URL: mustParseURL("/foo"), 359 ProtoMajor: 1, 360 ProtoMinor: 0, 361 Header: Header{ 362 "X-Foo": []string{"X-Bar"}, 363 }, 364 }, 365 366 WantWrite: "GET /foo HTTP/1.1\r\n" + 367 "Host: \r\n" + 368 "User-Agent: Go-http-client/1.1\r\n" + 369 "X-Foo: X-Bar\r\n\r\n", 370 }, 371 372 // If no Request.Host and no Request.URL.Host, we send 373 // an empty Host header, and don't use 374 // Request.Header["Host"]. This is just testing that 375 // we don't change Go 1.0 behavior. 376 { 377 Req: Request{ 378 Method: "GET", 379 Host: "", 380 URL: &url.URL{ 381 Scheme: "http", 382 Host: "", 383 Path: "/search", 384 }, 385 ProtoMajor: 1, 386 ProtoMinor: 1, 387 Header: Header{ 388 "Host": []string{"bad.example.com"}, 389 }, 390 }, 391 392 WantWrite: "GET /search HTTP/1.1\r\n" + 393 "Host: \r\n" + 394 "User-Agent: Go-http-client/1.1\r\n\r\n", 395 }, 396 397 // Opaque test #1 from golang.org/issue/4860 398 { 399 Req: Request{ 400 Method: "GET", 401 URL: &url.URL{ 402 Scheme: "http", 403 Host: "www.google.com", 404 Opaque: "/%2F/%2F/", 405 }, 406 ProtoMajor: 1, 407 ProtoMinor: 1, 408 Header: Header{}, 409 }, 410 411 WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" + 412 "Host: www.google.com\r\n" + 413 "User-Agent: Go-http-client/1.1\r\n\r\n", 414 }, 415 416 // Opaque test #2 from golang.org/issue/4860 417 { 418 Req: Request{ 419 Method: "GET", 420 URL: &url.URL{ 421 Scheme: "http", 422 Host: "x.google.com", 423 Opaque: "//y.google.com/%2F/%2F/", 424 }, 425 ProtoMajor: 1, 426 ProtoMinor: 1, 427 Header: Header{}, 428 }, 429 430 WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" + 431 "Host: x.google.com\r\n" + 432 "User-Agent: Go-http-client/1.1\r\n\r\n", 433 }, 434 435 // Testing custom case in header keys. Issue 5022. 436 { 437 Req: Request{ 438 Method: "GET", 439 URL: &url.URL{ 440 Scheme: "http", 441 Host: "www.google.com", 442 Path: "/", 443 }, 444 Proto: "HTTP/1.1", 445 ProtoMajor: 1, 446 ProtoMinor: 1, 447 Header: Header{ 448 "ALL-CAPS": {"x"}, 449 }, 450 }, 451 452 WantWrite: "GET / HTTP/1.1\r\n" + 453 "Host: www.google.com\r\n" + 454 "User-Agent: Go-http-client/1.1\r\n" + 455 "ALL-CAPS: x\r\n" + 456 "\r\n", 457 }, 458 459 // Request with host header field; IPv6 address with zone identifier 460 { 461 Req: Request{ 462 Method: "GET", 463 URL: &url.URL{ 464 Host: "[fe80::1%en0]", 465 }, 466 }, 467 468 WantWrite: "GET / HTTP/1.1\r\n" + 469 "Host: [fe80::1]\r\n" + 470 "User-Agent: Go-http-client/1.1\r\n" + 471 "\r\n", 472 }, 473 474 // Request with optional host header field; IPv6 address with zone identifier 475 { 476 Req: Request{ 477 Method: "GET", 478 URL: &url.URL{ 479 Host: "www.example.com", 480 }, 481 Host: "[fe80::1%en0]:8080", 482 }, 483 484 WantWrite: "GET / HTTP/1.1\r\n" + 485 "Host: [fe80::1]:8080\r\n" + 486 "User-Agent: Go-http-client/1.1\r\n" + 487 "\r\n", 488 }, 489 } 490 491 func TestRequestWrite(t *testing.T) { 492 for i := range reqWriteTests { 493 tt := &reqWriteTests[i] 494 495 setBody := func() { 496 if tt.Body == nil { 497 return 498 } 499 switch b := tt.Body.(type) { 500 case []byte: 501 tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b)) 502 case func() io.ReadCloser: 503 tt.Req.Body = b() 504 } 505 } 506 setBody() 507 if tt.Req.Header == nil { 508 tt.Req.Header = make(Header) 509 } 510 511 var braw bytes.Buffer 512 err := tt.Req.Write(&braw) 513 if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e { 514 t.Errorf("writing #%d, err = %q, want %q", i, g, e) 515 continue 516 } 517 if err != nil { 518 continue 519 } 520 521 if tt.WantWrite != "" { 522 sraw := braw.String() 523 if sraw != tt.WantWrite { 524 t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw) 525 continue 526 } 527 } 528 529 if tt.WantProxy != "" { 530 setBody() 531 var praw bytes.Buffer 532 err = tt.Req.WriteProxy(&praw) 533 if err != nil { 534 t.Errorf("WriteProxy #%d: %s", i, err) 535 continue 536 } 537 sraw := praw.String() 538 if sraw != tt.WantProxy { 539 t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw) 540 continue 541 } 542 } 543 } 544 } 545 546 type closeChecker struct { 547 io.Reader 548 closed bool 549 } 550 551 func (rc *closeChecker) Close() error { 552 rc.closed = true 553 return nil 554 } 555 556 // TestRequestWriteClosesBody tests that Request.Write does close its request.Body. 557 // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer 558 // inside a NopCloser, and that it serializes it correctly. 559 func TestRequestWriteClosesBody(t *testing.T) { 560 rc := &closeChecker{Reader: strings.NewReader("my body")} 561 req, _ := NewRequest("POST", "http://foo.com/", rc) 562 if req.ContentLength != 0 { 563 t.Errorf("got req.ContentLength %d, want 0", req.ContentLength) 564 } 565 buf := new(bytes.Buffer) 566 req.Write(buf) 567 if !rc.closed { 568 t.Error("body not closed after write") 569 } 570 expected := "POST / HTTP/1.1\r\n" + 571 "Host: foo.com\r\n" + 572 "User-Agent: Go-http-client/1.1\r\n" + 573 "Transfer-Encoding: chunked\r\n\r\n" + 574 // TODO: currently we don't buffer before chunking, so we get a 575 // single "m" chunk before the other chunks, as this was the 1-byte 576 // read from our MultiReader where we stiched the Body back together 577 // after sniffing whether the Body was 0 bytes or not. 578 chunk("m") + 579 chunk("y body") + 580 chunk("") 581 if buf.String() != expected { 582 t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected) 583 } 584 } 585 586 func chunk(s string) string { 587 return fmt.Sprintf("%x\r\n%s\r\n", len(s), s) 588 } 589 590 func mustParseURL(s string) *url.URL { 591 u, err := url.Parse(s) 592 if err != nil { 593 panic(fmt.Sprintf("Error parsing URL %q: %v", s, err)) 594 } 595 return u 596 } 597 598 type writerFunc func([]byte) (int, error) 599 600 func (f writerFunc) Write(p []byte) (int, error) { return f(p) } 601 602 // TestRequestWriteError tests the Write err != nil checks in (*Request).write. 603 func TestRequestWriteError(t *testing.T) { 604 failAfter, writeCount := 0, 0 605 errFail := errors.New("fake write failure") 606 607 // w is the buffered io.Writer to write the request to. It 608 // fails exactly once on its Nth Write call, as controlled by 609 // failAfter. It also tracks the number of calls in 610 // writeCount. 611 w := struct { 612 io.ByteWriter // to avoid being wrapped by a bufio.Writer 613 io.Writer 614 }{ 615 nil, 616 writerFunc(func(p []byte) (n int, err error) { 617 writeCount++ 618 if failAfter == 0 { 619 err = errFail 620 } 621 failAfter-- 622 return len(p), err 623 }), 624 } 625 626 req, _ := NewRequest("GET", "http://example.com/", nil) 627 const writeCalls = 4 // number of Write calls in current implementation 628 sawGood := false 629 for n := 0; n <= writeCalls+2; n++ { 630 failAfter = n 631 writeCount = 0 632 err := req.Write(w) 633 var wantErr error 634 if n < writeCalls { 635 wantErr = errFail 636 } 637 if err != wantErr { 638 t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr) 639 continue 640 } 641 if err == nil { 642 sawGood = true 643 if writeCount != writeCalls { 644 t.Fatalf("writeCalls constant is outdated in test") 645 } 646 } 647 if writeCount > writeCalls || writeCount > n+1 { 648 t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount) 649 } 650 } 651 if !sawGood { 652 t.Fatalf("writeCalls constant is outdated in test") 653 } 654 } 655