gitee.com/ks-custle/core-gm@v0.0.0-20230922171213-b83bdd97b62c/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 "gitee.com/ks-custle/core-gm/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 //goland:noinspection HttpUrlsUsage 764 var responseLocationTests = []responseLocationTest{ 765 {"/foo", "http://bar.com/baz", "http://bar.com/foo", nil}, 766 {"http://foo.com/", "http://bar.com/baz", "http://foo.com/", nil}, 767 {"", "http://bar.com/baz", "", ErrNoLocation}, 768 {"/bar", "", "/bar", nil}, 769 } 770 771 func TestLocationResponse(t *testing.T) { 772 for i, tt := range responseLocationTests { 773 res := new(Response) 774 res.Header = make(Header) 775 res.Header.Set("Location", tt.location) 776 if tt.requrl != "" { 777 res.Request = &Request{} 778 var err error 779 res.Request.URL, err = url.Parse(tt.requrl) 780 if err != nil { 781 t.Fatalf("bad test URL %q: %v", tt.requrl, err) 782 } 783 } 784 785 got, err := res.Location() 786 if tt.wantErr != nil { 787 if err == nil { 788 t.Errorf("%d. err=nil; want %q", i, tt.wantErr) 789 continue 790 } 791 if g, e := err.Error(), tt.wantErr.Error(); g != e { 792 t.Errorf("%d. err=%q; want %q", i, g, e) 793 continue 794 } 795 continue 796 } 797 if err != nil { 798 t.Errorf("%d. err=%q", i, err) 799 continue 800 } 801 if g, e := got.String(), tt.want; g != e { 802 t.Errorf("%d. Location=%q; want %q", i, g, e) 803 } 804 } 805 } 806 807 func TestResponseStatusStutter(t *testing.T) { 808 r := &Response{ 809 Status: "123 some status", 810 StatusCode: 123, 811 ProtoMajor: 1, 812 ProtoMinor: 3, 813 } 814 var buf bytes.Buffer 815 _ = r.Write(&buf) 816 if strings.Contains(buf.String(), "123 123") { 817 t.Errorf("stutter in status: %s", buf.String()) 818 } 819 } 820 821 func TestResponseContentLengthShortBody(t *testing.T) { 822 const shortBody = "Short body, not 123 bytes." 823 br := bufio.NewReader(strings.NewReader("HTTP/1.1 200 OK\r\n" + 824 "Content-Length: 123\r\n" + 825 "\r\n" + 826 shortBody)) 827 res, err := ReadResponse(br, &Request{Method: "GET"}) 828 if err != nil { 829 t.Fatal(err) 830 } 831 if res.ContentLength != 123 { 832 t.Fatalf("Content-Length = %d; want 123", res.ContentLength) 833 } 834 var buf bytes.Buffer 835 n, err := io.Copy(&buf, res.Body) 836 if n != int64(len(shortBody)) { 837 t.Errorf("Copied %d bytes; want %d, len(%q)", n, len(shortBody), shortBody) 838 } 839 if buf.String() != shortBody { 840 t.Errorf("Read body %q; want %q", buf.String(), shortBody) 841 } 842 if err != io.ErrUnexpectedEOF { 843 t.Errorf("io.Copy error = %#v; want io.ErrUnexpectedEOF", err) 844 } 845 } 846 847 // Test various ReadResponse error cases. (also tests success cases, but mostly 848 // it's about errors). This does not test anything involving the bodies. Only 849 // the return value from ReadResponse itself. 850 func TestReadResponseErrors(t *testing.T) { 851 type testCase struct { 852 name string // optional, defaults to in 853 in string 854 wantErr interface{} // nil, err value, or string substring 855 } 856 857 status := func(s string, wantErr interface{}) testCase { 858 if wantErr == true { 859 wantErr = "malformed HTTP status code" 860 } 861 return testCase{ 862 name: fmt.Sprintf("status %q", s), 863 in: "HTTP/1.1 " + s + "\r\nFoo: bar\r\n\r\n", 864 wantErr: wantErr, 865 } 866 } 867 868 version := func(s string, wantErr interface{}) testCase { 869 if wantErr == true { 870 wantErr = "malformed HTTP version" 871 } 872 return testCase{ 873 name: fmt.Sprintf("version %q", s), 874 in: s + " 200 OK\r\n\r\n", 875 wantErr: wantErr, 876 } 877 } 878 879 contentLength := func(status, body string, wantErr interface{}) testCase { 880 return testCase{ 881 name: fmt.Sprintf("status %q %q", status, body), 882 in: fmt.Sprintf("HTTP/1.1 %s\r\n%s", status, body), 883 wantErr: wantErr, 884 } 885 } 886 887 errMultiCL := "message cannot contain multiple Content-Length headers" 888 889 tests := []testCase{ 890 {"", "", io.ErrUnexpectedEOF}, 891 {"", "HTTP/1.1 301 Moved Permanently\r\nFoo: bar", io.ErrUnexpectedEOF}, 892 {"", "HTTP/1.1", "malformed HTTP response"}, 893 {"", "HTTP/2.0", "malformed HTTP response"}, 894 status("20X Unknown", true), 895 status("abcd Unknown", true), 896 status("二百/两百 OK", true), 897 status(" Unknown", true), 898 status("c8 OK", true), 899 status("0x12d Moved Permanently", true), 900 status("200 OK", nil), 901 status("000 OK", nil), 902 status("001 OK", nil), 903 status("404 NOTFOUND", nil), 904 status("20 OK", true), 905 status("00 OK", true), 906 status("-10 OK", true), 907 status("1000 OK", true), 908 status("999 Done", nil), 909 status("-1 OK", true), 910 status("-200 OK", true), 911 version("HTTP/1.2", nil), 912 version("HTTP/2.0", nil), 913 version("HTTP/1.100000000002", true), 914 version("HTTP/1.-1", true), 915 version("HTTP/A.B", true), 916 version("HTTP/1", true), 917 version("http/1.1", true), 918 919 contentLength("200 OK", "Content-Length: 10\r\nContent-Length: 7\r\n\r\nGopher hey\r\n", errMultiCL), 920 contentLength("200 OK", "Content-Length: 7\r\nContent-Length: 7\r\n\r\nGophers\r\n", nil), 921 contentLength("201 OK", "Content-Length: 0\r\nContent-Length: 7\r\n\r\nGophers\r\n", errMultiCL), 922 contentLength("300 OK", "Content-Length: 0\r\nContent-Length: 0 \r\n\r\nGophers\r\n", nil), 923 contentLength("200 OK", "Content-Length:\r\nContent-Length:\r\n\r\nGophers\r\n", nil), 924 contentLength("206 OK", "Content-Length:\r\nContent-Length: 0 \r\nConnection: close\r\n\r\nGophers\r\n", errMultiCL), 925 926 // multiple content-length headers for 204 and 304 should still be checked 927 contentLength("204 OK", "Content-Length: 7\r\nContent-Length: 8\r\n\r\n", errMultiCL), 928 contentLength("204 OK", "Content-Length: 3\r\nContent-Length: 3\r\n\r\n", nil), 929 contentLength("304 OK", "Content-Length: 880\r\nContent-Length: 1\r\n\r\n", errMultiCL), 930 contentLength("304 OK", "Content-Length: 961\r\nContent-Length: 961\r\n\r\n", nil), 931 932 // golang.org/issue/22464 933 {"leading space in header", "HTTP/1.1 200 OK\r\n Content-type: text/html\r\nFoo: bar\r\n\r\n", "malformed MIME"}, 934 {"leading tab in header", "HTTP/1.1 200 OK\r\n\tContent-type: text/html\r\nFoo: bar\r\n\r\n", "malformed MIME"}, 935 } 936 937 for i, tt := range tests { 938 br := bufio.NewReader(strings.NewReader(tt.in)) 939 _, rerr := ReadResponse(br, nil) 940 if err := matchErr(rerr, tt.wantErr); err != nil { 941 name := tt.name 942 if name == "" { 943 name = fmt.Sprintf("%d. input %q", i, tt.in) 944 } 945 t.Errorf("%s: %v", name, err) 946 } 947 } 948 } 949 950 // wantErr can be nil, an error value to match exactly, or type string to 951 // match a substring. 952 func matchErr(err error, wantErr interface{}) error { 953 if err == nil { 954 if wantErr == nil { 955 return nil 956 } 957 if sub, ok := wantErr.(string); ok { 958 return fmt.Errorf("unexpected success; want error with substring %q", sub) 959 } 960 return fmt.Errorf("unexpected success; want error %v", wantErr) 961 } 962 if wantErr == nil { 963 return fmt.Errorf("%v; want success", err) 964 } 965 if sub, ok := wantErr.(string); ok { 966 if strings.Contains(err.Error(), sub) { 967 return nil 968 } 969 return fmt.Errorf("error = %v; want an error with substring %q", err, sub) 970 } 971 if err == wantErr { 972 return nil 973 } 974 return fmt.Errorf("%v; want %v", err, wantErr) 975 } 976 977 //goland:noinspection GoBoolExpressions 978 func TestNeedsSniff(t *testing.T) { 979 // needsSniff returns true with an empty response. 980 r := &response{} 981 if got, want := r.needsSniff(), true; got != want { 982 t.Errorf("needsSniff = %t; want %t", got, want) 983 } 984 // needsSniff returns false when Content-Type = nil. 985 r.handlerHeader = Header{"Content-Type": nil} 986 if got, want := r.needsSniff(), false; got != want { 987 t.Errorf("needsSniff empty Content-Type = %t; want %t", got, want) 988 } 989 } 990 991 // A response should only write out single Connection: close header. Tests #19499. 992 func TestResponseWritesOnlySingleConnectionClose(t *testing.T) { 993 const connectionCloseHeader = "Connection: close" 994 995 res, err := ReadResponse(bufio.NewReader(strings.NewReader("HTTP/1.0 200 OK\r\n\r\nAAAA")), nil) 996 if err != nil { 997 t.Fatalf("ReadResponse failed %v", err) 998 } 999 1000 var buf1 bytes.Buffer 1001 if err = res.Write(&buf1); err != nil { 1002 t.Fatalf("Write failed %v", err) 1003 } 1004 if res, err = ReadResponse(bufio.NewReader(&buf1), nil); err != nil { 1005 t.Fatalf("ReadResponse failed %v", err) 1006 } 1007 1008 var buf2 bytes.Buffer 1009 if err = res.Write(&buf2); err != nil { 1010 t.Fatalf("Write failed %v", err) 1011 } 1012 if count := strings.Count(buf2.String(), connectionCloseHeader); count != 1 { 1013 t.Errorf("Found %d %q header", count, connectionCloseHeader) 1014 } 1015 }