github.com/tidwall/go@v0.0.0-20170415222209-6694a6888b7d/src/net/http/response_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 "compress/gzip" 11 "crypto/rand" 12 "fmt" 13 "go/ast" 14 "io" 15 "io/ioutil" 16 "net/http/internal" 17 "net/url" 18 "reflect" 19 "regexp" 20 "strings" 21 "testing" 22 ) 23 24 type respTest struct { 25 Raw string 26 Resp Response 27 Body string 28 } 29 30 func dummyReq(method string) *Request { 31 return &Request{Method: method} 32 } 33 34 func dummyReq11(method string) *Request { 35 return &Request{Method: method, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1} 36 } 37 38 var respTests = []respTest{ 39 // Unchunked response without Content-Length. 40 { 41 "HTTP/1.0 200 OK\r\n" + 42 "Connection: close\r\n" + 43 "\r\n" + 44 "Body here\n", 45 46 Response{ 47 Status: "200 OK", 48 StatusCode: 200, 49 Proto: "HTTP/1.0", 50 ProtoMajor: 1, 51 ProtoMinor: 0, 52 Request: dummyReq("GET"), 53 Header: Header{ 54 "Connection": {"close"}, // TODO(rsc): Delete? 55 }, 56 Close: true, 57 ContentLength: -1, 58 }, 59 60 "Body here\n", 61 }, 62 63 // Unchunked HTTP/1.1 response without Content-Length or 64 // Connection headers. 65 { 66 "HTTP/1.1 200 OK\r\n" + 67 "\r\n" + 68 "Body here\n", 69 70 Response{ 71 Status: "200 OK", 72 StatusCode: 200, 73 Proto: "HTTP/1.1", 74 ProtoMajor: 1, 75 ProtoMinor: 1, 76 Header: Header{}, 77 Request: dummyReq("GET"), 78 Close: true, 79 ContentLength: -1, 80 }, 81 82 "Body here\n", 83 }, 84 85 // Unchunked HTTP/1.1 204 response without Content-Length. 86 { 87 "HTTP/1.1 204 No Content\r\n" + 88 "\r\n" + 89 "Body should not be read!\n", 90 91 Response{ 92 Status: "204 No Content", 93 StatusCode: 204, 94 Proto: "HTTP/1.1", 95 ProtoMajor: 1, 96 ProtoMinor: 1, 97 Header: Header{}, 98 Request: dummyReq("GET"), 99 Close: false, 100 ContentLength: 0, 101 }, 102 103 "", 104 }, 105 106 // Unchunked response with Content-Length. 107 { 108 "HTTP/1.0 200 OK\r\n" + 109 "Content-Length: 10\r\n" + 110 "Connection: close\r\n" + 111 "\r\n" + 112 "Body here\n", 113 114 Response{ 115 Status: "200 OK", 116 StatusCode: 200, 117 Proto: "HTTP/1.0", 118 ProtoMajor: 1, 119 ProtoMinor: 0, 120 Request: dummyReq("GET"), 121 Header: Header{ 122 "Connection": {"close"}, 123 "Content-Length": {"10"}, 124 }, 125 Close: true, 126 ContentLength: 10, 127 }, 128 129 "Body here\n", 130 }, 131 132 // Chunked response without Content-Length. 133 { 134 "HTTP/1.1 200 OK\r\n" + 135 "Transfer-Encoding: chunked\r\n" + 136 "\r\n" + 137 "0a\r\n" + 138 "Body here\n\r\n" + 139 "09\r\n" + 140 "continued\r\n" + 141 "0\r\n" + 142 "\r\n", 143 144 Response{ 145 Status: "200 OK", 146 StatusCode: 200, 147 Proto: "HTTP/1.1", 148 ProtoMajor: 1, 149 ProtoMinor: 1, 150 Request: dummyReq("GET"), 151 Header: Header{}, 152 Close: false, 153 ContentLength: -1, 154 TransferEncoding: []string{"chunked"}, 155 }, 156 157 "Body here\ncontinued", 158 }, 159 160 // Chunked response with Content-Length. 161 { 162 "HTTP/1.1 200 OK\r\n" + 163 "Transfer-Encoding: chunked\r\n" + 164 "Content-Length: 10\r\n" + 165 "\r\n" + 166 "0a\r\n" + 167 "Body here\n\r\n" + 168 "0\r\n" + 169 "\r\n", 170 171 Response{ 172 Status: "200 OK", 173 StatusCode: 200, 174 Proto: "HTTP/1.1", 175 ProtoMajor: 1, 176 ProtoMinor: 1, 177 Request: dummyReq("GET"), 178 Header: Header{}, 179 Close: false, 180 ContentLength: -1, 181 TransferEncoding: []string{"chunked"}, 182 }, 183 184 "Body here\n", 185 }, 186 187 // Chunked response in response to a HEAD request 188 { 189 "HTTP/1.1 200 OK\r\n" + 190 "Transfer-Encoding: chunked\r\n" + 191 "\r\n", 192 193 Response{ 194 Status: "200 OK", 195 StatusCode: 200, 196 Proto: "HTTP/1.1", 197 ProtoMajor: 1, 198 ProtoMinor: 1, 199 Request: dummyReq("HEAD"), 200 Header: Header{}, 201 TransferEncoding: []string{"chunked"}, 202 Close: false, 203 ContentLength: -1, 204 }, 205 206 "", 207 }, 208 209 // Content-Length in response to a HEAD request 210 { 211 "HTTP/1.0 200 OK\r\n" + 212 "Content-Length: 256\r\n" + 213 "\r\n", 214 215 Response{ 216 Status: "200 OK", 217 StatusCode: 200, 218 Proto: "HTTP/1.0", 219 ProtoMajor: 1, 220 ProtoMinor: 0, 221 Request: dummyReq("HEAD"), 222 Header: Header{"Content-Length": {"256"}}, 223 TransferEncoding: nil, 224 Close: true, 225 ContentLength: 256, 226 }, 227 228 "", 229 }, 230 231 // Content-Length in response to a HEAD request with HTTP/1.1 232 { 233 "HTTP/1.1 200 OK\r\n" + 234 "Content-Length: 256\r\n" + 235 "\r\n", 236 237 Response{ 238 Status: "200 OK", 239 StatusCode: 200, 240 Proto: "HTTP/1.1", 241 ProtoMajor: 1, 242 ProtoMinor: 1, 243 Request: dummyReq("HEAD"), 244 Header: Header{"Content-Length": {"256"}}, 245 TransferEncoding: nil, 246 Close: false, 247 ContentLength: 256, 248 }, 249 250 "", 251 }, 252 253 // No Content-Length or Chunked in response to a HEAD request 254 { 255 "HTTP/1.0 200 OK\r\n" + 256 "\r\n", 257 258 Response{ 259 Status: "200 OK", 260 StatusCode: 200, 261 Proto: "HTTP/1.0", 262 ProtoMajor: 1, 263 ProtoMinor: 0, 264 Request: dummyReq("HEAD"), 265 Header: Header{}, 266 TransferEncoding: nil, 267 Close: true, 268 ContentLength: -1, 269 }, 270 271 "", 272 }, 273 274 // explicit Content-Length of 0. 275 { 276 "HTTP/1.1 200 OK\r\n" + 277 "Content-Length: 0\r\n" + 278 "\r\n", 279 280 Response{ 281 Status: "200 OK", 282 StatusCode: 200, 283 Proto: "HTTP/1.1", 284 ProtoMajor: 1, 285 ProtoMinor: 1, 286 Request: dummyReq("GET"), 287 Header: Header{ 288 "Content-Length": {"0"}, 289 }, 290 Close: false, 291 ContentLength: 0, 292 }, 293 294 "", 295 }, 296 297 // Status line without a Reason-Phrase, but trailing space. 298 // (permitted by RFC 2616) 299 { 300 "HTTP/1.0 303 \r\n\r\n", 301 Response{ 302 Status: "303 ", 303 StatusCode: 303, 304 Proto: "HTTP/1.0", 305 ProtoMajor: 1, 306 ProtoMinor: 0, 307 Request: dummyReq("GET"), 308 Header: Header{}, 309 Close: true, 310 ContentLength: -1, 311 }, 312 313 "", 314 }, 315 316 // Status line without a Reason-Phrase, and no trailing space. 317 // (not permitted by RFC 2616, but we'll accept it anyway) 318 { 319 "HTTP/1.0 303\r\n\r\n", 320 Response{ 321 Status: "303 ", 322 StatusCode: 303, 323 Proto: "HTTP/1.0", 324 ProtoMajor: 1, 325 ProtoMinor: 0, 326 Request: dummyReq("GET"), 327 Header: Header{}, 328 Close: true, 329 ContentLength: -1, 330 }, 331 332 "", 333 }, 334 335 // golang.org/issue/4767: don't special-case multipart/byteranges responses 336 { 337 `HTTP/1.1 206 Partial Content 338 Connection: close 339 Content-Type: multipart/byteranges; boundary=18a75608c8f47cef 340 341 some body`, 342 Response{ 343 Status: "206 Partial Content", 344 StatusCode: 206, 345 Proto: "HTTP/1.1", 346 ProtoMajor: 1, 347 ProtoMinor: 1, 348 Request: dummyReq("GET"), 349 Header: Header{ 350 "Content-Type": []string{"multipart/byteranges; boundary=18a75608c8f47cef"}, 351 }, 352 Close: true, 353 ContentLength: -1, 354 }, 355 356 "some body", 357 }, 358 359 // Unchunked response without Content-Length, Request is nil 360 { 361 "HTTP/1.0 200 OK\r\n" + 362 "Connection: close\r\n" + 363 "\r\n" + 364 "Body here\n", 365 366 Response{ 367 Status: "200 OK", 368 StatusCode: 200, 369 Proto: "HTTP/1.0", 370 ProtoMajor: 1, 371 ProtoMinor: 0, 372 Header: Header{ 373 "Connection": {"close"}, // TODO(rsc): Delete? 374 }, 375 Close: true, 376 ContentLength: -1, 377 }, 378 379 "Body here\n", 380 }, 381 382 // 206 Partial Content. golang.org/issue/8923 383 { 384 "HTTP/1.1 206 Partial Content\r\n" + 385 "Content-Type: text/plain; charset=utf-8\r\n" + 386 "Accept-Ranges: bytes\r\n" + 387 "Content-Range: bytes 0-5/1862\r\n" + 388 "Content-Length: 6\r\n\r\n" + 389 "foobar", 390 391 Response{ 392 Status: "206 Partial Content", 393 StatusCode: 206, 394 Proto: "HTTP/1.1", 395 ProtoMajor: 1, 396 ProtoMinor: 1, 397 Request: dummyReq("GET"), 398 Header: Header{ 399 "Accept-Ranges": []string{"bytes"}, 400 "Content-Length": []string{"6"}, 401 "Content-Type": []string{"text/plain; charset=utf-8"}, 402 "Content-Range": []string{"bytes 0-5/1862"}, 403 }, 404 ContentLength: 6, 405 }, 406 407 "foobar", 408 }, 409 410 // Both keep-alive and close, on the same Connection line. (Issue 8840) 411 { 412 "HTTP/1.1 200 OK\r\n" + 413 "Content-Length: 256\r\n" + 414 "Connection: keep-alive, close\r\n" + 415 "\r\n", 416 417 Response{ 418 Status: "200 OK", 419 StatusCode: 200, 420 Proto: "HTTP/1.1", 421 ProtoMajor: 1, 422 ProtoMinor: 1, 423 Request: dummyReq("HEAD"), 424 Header: Header{ 425 "Content-Length": {"256"}, 426 }, 427 TransferEncoding: nil, 428 Close: true, 429 ContentLength: 256, 430 }, 431 432 "", 433 }, 434 435 // Both keep-alive and close, on different Connection lines. (Issue 8840) 436 { 437 "HTTP/1.1 200 OK\r\n" + 438 "Content-Length: 256\r\n" + 439 "Connection: keep-alive\r\n" + 440 "Connection: close\r\n" + 441 "\r\n", 442 443 Response{ 444 Status: "200 OK", 445 StatusCode: 200, 446 Proto: "HTTP/1.1", 447 ProtoMajor: 1, 448 ProtoMinor: 1, 449 Request: dummyReq("HEAD"), 450 Header: Header{ 451 "Content-Length": {"256"}, 452 }, 453 TransferEncoding: nil, 454 Close: true, 455 ContentLength: 256, 456 }, 457 458 "", 459 }, 460 461 // Issue 12785: HTTP/1.0 response with bogus (to be ignored) Transfer-Encoding. 462 // Without a Content-Length. 463 { 464 "HTTP/1.0 200 OK\r\n" + 465 "Transfer-Encoding: bogus\r\n" + 466 "\r\n" + 467 "Body here\n", 468 469 Response{ 470 Status: "200 OK", 471 StatusCode: 200, 472 Proto: "HTTP/1.0", 473 ProtoMajor: 1, 474 ProtoMinor: 0, 475 Request: dummyReq("GET"), 476 Header: Header{}, 477 Close: true, 478 ContentLength: -1, 479 }, 480 481 "Body here\n", 482 }, 483 484 // Issue 12785: HTTP/1.0 response with bogus (to be ignored) Transfer-Encoding. 485 // With a Content-Length. 486 { 487 "HTTP/1.0 200 OK\r\n" + 488 "Transfer-Encoding: bogus\r\n" + 489 "Content-Length: 10\r\n" + 490 "\r\n" + 491 "Body here\n", 492 493 Response{ 494 Status: "200 OK", 495 StatusCode: 200, 496 Proto: "HTTP/1.0", 497 ProtoMajor: 1, 498 ProtoMinor: 0, 499 Request: dummyReq("GET"), 500 Header: Header{ 501 "Content-Length": {"10"}, 502 }, 503 Close: true, 504 ContentLength: 10, 505 }, 506 507 "Body here\n", 508 }, 509 510 { 511 "HTTP/1.1 200 OK\r\n" + 512 "Content-Encoding: gzip\r\n" + 513 "Content-Length: 23\r\n" + 514 "Connection: keep-alive\r\n" + 515 "Keep-Alive: timeout=7200\r\n\r\n" + 516 "\x1f\x8b\b\x00\x00\x00\x00\x00\x00\x00s\xf3\xf7\a\x00\xab'\xd4\x1a\x03\x00\x00\x00", 517 Response{ 518 Status: "200 OK", 519 StatusCode: 200, 520 Proto: "HTTP/1.1", 521 ProtoMajor: 1, 522 ProtoMinor: 1, 523 Request: dummyReq("GET"), 524 Header: Header{ 525 "Content-Length": {"23"}, 526 "Content-Encoding": {"gzip"}, 527 "Connection": {"keep-alive"}, 528 "Keep-Alive": {"timeout=7200"}, 529 }, 530 Close: false, 531 ContentLength: 23, 532 }, 533 "\x1f\x8b\b\x00\x00\x00\x00\x00\x00\x00s\xf3\xf7\a\x00\xab'\xd4\x1a\x03\x00\x00\x00", 534 }, 535 } 536 537 // tests successful calls to ReadResponse, and inspects the returned Response. 538 // For error cases, see TestReadResponseErrors below. 539 func TestReadResponse(t *testing.T) { 540 for i, tt := range respTests { 541 resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request) 542 if err != nil { 543 t.Errorf("#%d: %v", i, err) 544 continue 545 } 546 rbody := resp.Body 547 resp.Body = nil 548 diff(t, fmt.Sprintf("#%d Response", i), resp, &tt.Resp) 549 var bout bytes.Buffer 550 if rbody != nil { 551 _, err = io.Copy(&bout, rbody) 552 if err != nil { 553 t.Errorf("#%d: %v", i, err) 554 continue 555 } 556 rbody.Close() 557 } 558 body := bout.String() 559 if body != tt.Body { 560 t.Errorf("#%d: Body = %q want %q", i, body, tt.Body) 561 } 562 } 563 } 564 565 func TestWriteResponse(t *testing.T) { 566 for i, tt := range respTests { 567 resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request) 568 if err != nil { 569 t.Errorf("#%d: %v", i, err) 570 continue 571 } 572 err = resp.Write(ioutil.Discard) 573 if err != nil { 574 t.Errorf("#%d: %v", i, err) 575 continue 576 } 577 } 578 } 579 580 var readResponseCloseInMiddleTests = []struct { 581 chunked, compressed bool 582 }{ 583 {false, false}, 584 {true, false}, 585 {true, true}, 586 } 587 588 // TestReadResponseCloseInMiddle tests that closing a body after 589 // reading only part of its contents advances the read to the end of 590 // the request, right up until the next request. 591 func TestReadResponseCloseInMiddle(t *testing.T) { 592 t.Parallel() 593 for _, test := range readResponseCloseInMiddleTests { 594 fatalf := func(format string, args ...interface{}) { 595 args = append([]interface{}{test.chunked, test.compressed}, args...) 596 t.Fatalf("on test chunked=%v, compressed=%v: "+format, args...) 597 } 598 checkErr := func(err error, msg string) { 599 if err == nil { 600 return 601 } 602 fatalf(msg+": %v", err) 603 } 604 var buf bytes.Buffer 605 buf.WriteString("HTTP/1.1 200 OK\r\n") 606 if test.chunked { 607 buf.WriteString("Transfer-Encoding: chunked\r\n") 608 } else { 609 buf.WriteString("Content-Length: 1000000\r\n") 610 } 611 var wr io.Writer = &buf 612 if test.chunked { 613 wr = internal.NewChunkedWriter(wr) 614 } 615 if test.compressed { 616 buf.WriteString("Content-Encoding: gzip\r\n") 617 wr = gzip.NewWriter(wr) 618 } 619 buf.WriteString("\r\n") 620 621 chunk := bytes.Repeat([]byte{'x'}, 1000) 622 for i := 0; i < 1000; i++ { 623 if test.compressed { 624 // Otherwise this compresses too well. 625 _, err := io.ReadFull(rand.Reader, chunk) 626 checkErr(err, "rand.Reader ReadFull") 627 } 628 wr.Write(chunk) 629 } 630 if test.compressed { 631 err := wr.(*gzip.Writer).Close() 632 checkErr(err, "compressor close") 633 } 634 if test.chunked { 635 buf.WriteString("0\r\n\r\n") 636 } 637 buf.WriteString("Next Request Here") 638 639 bufr := bufio.NewReader(&buf) 640 resp, err := ReadResponse(bufr, dummyReq("GET")) 641 checkErr(err, "ReadResponse") 642 expectedLength := int64(-1) 643 if !test.chunked { 644 expectedLength = 1000000 645 } 646 if resp.ContentLength != expectedLength { 647 fatalf("expected response length %d, got %d", expectedLength, resp.ContentLength) 648 } 649 if resp.Body == nil { 650 fatalf("nil body") 651 } 652 if test.compressed { 653 gzReader, err := gzip.NewReader(resp.Body) 654 checkErr(err, "gzip.NewReader") 655 resp.Body = &readerAndCloser{gzReader, resp.Body} 656 } 657 658 rbuf := make([]byte, 2500) 659 n, err := io.ReadFull(resp.Body, rbuf) 660 checkErr(err, "2500 byte ReadFull") 661 if n != 2500 { 662 fatalf("ReadFull only read %d bytes", n) 663 } 664 if test.compressed == false && !bytes.Equal(bytes.Repeat([]byte{'x'}, 2500), rbuf) { 665 fatalf("ReadFull didn't read 2500 'x'; got %q", string(rbuf)) 666 } 667 resp.Body.Close() 668 669 rest, err := ioutil.ReadAll(bufr) 670 checkErr(err, "ReadAll on remainder") 671 if e, g := "Next Request Here", string(rest); e != g { 672 g = regexp.MustCompile(`(xx+)`).ReplaceAllStringFunc(g, func(match string) string { 673 return fmt.Sprintf("x(repeated x%d)", len(match)) 674 }) 675 fatalf("remainder = %q, expected %q", g, e) 676 } 677 } 678 } 679 680 func diff(t *testing.T, prefix string, have, want interface{}) { 681 hv := reflect.ValueOf(have).Elem() 682 wv := reflect.ValueOf(want).Elem() 683 if hv.Type() != wv.Type() { 684 t.Errorf("%s: type mismatch %v want %v", prefix, hv.Type(), wv.Type()) 685 } 686 for i := 0; i < hv.NumField(); i++ { 687 name := hv.Type().Field(i).Name 688 if !ast.IsExported(name) { 689 continue 690 } 691 hf := hv.Field(i).Interface() 692 wf := wv.Field(i).Interface() 693 if !reflect.DeepEqual(hf, wf) { 694 t.Errorf("%s: %s = %v want %v", prefix, name, hf, wf) 695 } 696 } 697 } 698 699 type responseLocationTest struct { 700 location string // Response's Location header or "" 701 requrl string // Response.Request.URL or "" 702 want string 703 wantErr error 704 } 705 706 var responseLocationTests = []responseLocationTest{ 707 {"/foo", "http://bar.com/baz", "http://bar.com/foo", nil}, 708 {"http://foo.com/", "http://bar.com/baz", "http://foo.com/", nil}, 709 {"", "http://bar.com/baz", "", ErrNoLocation}, 710 {"/bar", "", "/bar", nil}, 711 } 712 713 func TestLocationResponse(t *testing.T) { 714 for i, tt := range responseLocationTests { 715 res := new(Response) 716 res.Header = make(Header) 717 res.Header.Set("Location", tt.location) 718 if tt.requrl != "" { 719 res.Request = &Request{} 720 var err error 721 res.Request.URL, err = url.Parse(tt.requrl) 722 if err != nil { 723 t.Fatalf("bad test URL %q: %v", tt.requrl, err) 724 } 725 } 726 727 got, err := res.Location() 728 if tt.wantErr != nil { 729 if err == nil { 730 t.Errorf("%d. err=nil; want %q", i, tt.wantErr) 731 continue 732 } 733 if g, e := err.Error(), tt.wantErr.Error(); g != e { 734 t.Errorf("%d. err=%q; want %q", i, g, e) 735 continue 736 } 737 continue 738 } 739 if err != nil { 740 t.Errorf("%d. err=%q", i, err) 741 continue 742 } 743 if g, e := got.String(), tt.want; g != e { 744 t.Errorf("%d. Location=%q; want %q", i, g, e) 745 } 746 } 747 } 748 749 func TestResponseStatusStutter(t *testing.T) { 750 r := &Response{ 751 Status: "123 some status", 752 StatusCode: 123, 753 ProtoMajor: 1, 754 ProtoMinor: 3, 755 } 756 var buf bytes.Buffer 757 r.Write(&buf) 758 if strings.Contains(buf.String(), "123 123") { 759 t.Errorf("stutter in status: %s", buf.String()) 760 } 761 } 762 763 func TestResponseContentLengthShortBody(t *testing.T) { 764 const shortBody = "Short body, not 123 bytes." 765 br := bufio.NewReader(strings.NewReader("HTTP/1.1 200 OK\r\n" + 766 "Content-Length: 123\r\n" + 767 "\r\n" + 768 shortBody)) 769 res, err := ReadResponse(br, &Request{Method: "GET"}) 770 if err != nil { 771 t.Fatal(err) 772 } 773 if res.ContentLength != 123 { 774 t.Fatalf("Content-Length = %d; want 123", res.ContentLength) 775 } 776 var buf bytes.Buffer 777 n, err := io.Copy(&buf, res.Body) 778 if n != int64(len(shortBody)) { 779 t.Errorf("Copied %d bytes; want %d, len(%q)", n, len(shortBody), shortBody) 780 } 781 if buf.String() != shortBody { 782 t.Errorf("Read body %q; want %q", buf.String(), shortBody) 783 } 784 if err != io.ErrUnexpectedEOF { 785 t.Errorf("io.Copy error = %#v; want io.ErrUnexpectedEOF", err) 786 } 787 } 788 789 // Test various ReadResponse error cases. (also tests success cases, but mostly 790 // it's about errors). This does not test anything involving the bodies. Only 791 // the return value from ReadResponse itself. 792 func TestReadResponseErrors(t *testing.T) { 793 type testCase struct { 794 name string // optional, defaults to in 795 in string 796 header Header 797 wantErr interface{} // nil, err value, or string substring 798 } 799 800 status := func(s string, wantErr interface{}) testCase { 801 if wantErr == true { 802 wantErr = "malformed HTTP status code" 803 } 804 return testCase{ 805 name: fmt.Sprintf("status %q", s), 806 in: "HTTP/1.1 " + s + "\r\nFoo: bar\r\n\r\n", 807 wantErr: wantErr, 808 } 809 } 810 811 version := func(s string, wantErr interface{}) testCase { 812 if wantErr == true { 813 wantErr = "malformed HTTP version" 814 } 815 return testCase{ 816 name: fmt.Sprintf("version %q", s), 817 in: s + " 200 OK\r\n\r\n", 818 wantErr: wantErr, 819 } 820 } 821 822 contentLength := func(status, body string, wantErr interface{}, header Header) testCase { 823 return testCase{ 824 name: fmt.Sprintf("status %q %q", status, body), 825 in: fmt.Sprintf("HTTP/1.1 %s\r\n%s", status, body), 826 wantErr: wantErr, 827 header: header, 828 } 829 } 830 831 errMultiCL := "message cannot contain multiple Content-Length headers" 832 833 tests := []testCase{ 834 {"", "", nil, io.ErrUnexpectedEOF}, 835 {"", "HTTP/1.1 301 Moved Permanently\r\nFoo: bar", nil, io.ErrUnexpectedEOF}, 836 {"", "HTTP/1.1", nil, "malformed HTTP response"}, 837 {"", "HTTP/2.0", nil, "malformed HTTP response"}, 838 status("20X Unknown", true), 839 status("abcd Unknown", true), 840 status("二百/两百 OK", true), 841 status(" Unknown", true), 842 status("c8 OK", true), 843 status("0x12d Moved Permanently", true), 844 status("200 OK", nil), 845 status("000 OK", nil), 846 status("001 OK", nil), 847 status("404 NOTFOUND", nil), 848 status("20 OK", true), 849 status("00 OK", true), 850 status("-10 OK", true), 851 status("1000 OK", true), 852 status("999 Done", nil), 853 status("-1 OK", true), 854 status("-200 OK", true), 855 version("HTTP/1.2", nil), 856 version("HTTP/2.0", nil), 857 version("HTTP/1.100000000002", true), 858 version("HTTP/1.-1", true), 859 version("HTTP/A.B", true), 860 version("HTTP/1", true), 861 version("http/1.1", true), 862 863 contentLength("200 OK", "Content-Length: 10\r\nContent-Length: 7\r\n\r\nGopher hey\r\n", errMultiCL, nil), 864 contentLength("200 OK", "Content-Length: 7\r\nContent-Length: 7\r\n\r\nGophers\r\n", nil, Header{"Content-Length": {"7"}}), 865 contentLength("201 OK", "Content-Length: 0\r\nContent-Length: 7\r\n\r\nGophers\r\n", errMultiCL, nil), 866 contentLength("300 OK", "Content-Length: 0\r\nContent-Length: 0 \r\n\r\nGophers\r\n", nil, Header{"Content-Length": {"0"}}), 867 contentLength("200 OK", "Content-Length:\r\nContent-Length:\r\n\r\nGophers\r\n", nil, nil), 868 contentLength("206 OK", "Content-Length:\r\nContent-Length: 0 \r\nConnection: close\r\n\r\nGophers\r\n", errMultiCL, nil), 869 870 // multiple content-length headers for 204 and 304 should still be checked 871 contentLength("204 OK", "Content-Length: 7\r\nContent-Length: 8\r\n\r\n", errMultiCL, nil), 872 contentLength("204 OK", "Content-Length: 3\r\nContent-Length: 3\r\n\r\n", nil, nil), 873 contentLength("304 OK", "Content-Length: 880\r\nContent-Length: 1\r\n\r\n", errMultiCL, nil), 874 contentLength("304 OK", "Content-Length: 961\r\nContent-Length: 961\r\n\r\n", nil, nil), 875 } 876 877 for i, tt := range tests { 878 br := bufio.NewReader(strings.NewReader(tt.in)) 879 _, rerr := ReadResponse(br, nil) 880 if err := matchErr(rerr, tt.wantErr); err != nil { 881 name := tt.name 882 if name == "" { 883 name = fmt.Sprintf("%d. input %q", i, tt.in) 884 } 885 t.Errorf("%s: %v", name, err) 886 } 887 } 888 } 889 890 // wantErr can be nil, an error value to match exactly, or type string to 891 // match a substring. 892 func matchErr(err error, wantErr interface{}) error { 893 if err == nil { 894 if wantErr == nil { 895 return nil 896 } 897 if sub, ok := wantErr.(string); ok { 898 return fmt.Errorf("unexpected success; want error with substring %q", sub) 899 } 900 return fmt.Errorf("unexpected success; want error %v", wantErr) 901 } 902 if wantErr == nil { 903 return fmt.Errorf("%v; want success", err) 904 } 905 if sub, ok := wantErr.(string); ok { 906 if strings.Contains(err.Error(), sub) { 907 return nil 908 } 909 return fmt.Errorf("error = %v; want an error with substring %q", err, sub) 910 } 911 if err == wantErr { 912 return nil 913 } 914 return fmt.Errorf("%v; want %v", err, wantErr) 915 } 916 917 func TestNeedsSniff(t *testing.T) { 918 // needsSniff returns true with an empty response. 919 r := &response{} 920 if got, want := r.needsSniff(), true; got != want { 921 t.Errorf("needsSniff = %t; want %t", got, want) 922 } 923 // needsSniff returns false when Content-Type = nil. 924 r.handlerHeader = Header{"Content-Type": nil} 925 if got, want := r.needsSniff(), false; got != want { 926 t.Errorf("needsSniff empty Content-Type = %t; want %t", got, want) 927 } 928 }