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