123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- // Copyright 2011 The Go Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
- package httputil
- import (
- "bufio"
- "bytes"
- "context"
- "fmt"
- "io"
- "math/rand"
- "net/http"
- "net/url"
- "runtime"
- "runtime/pprof"
- "strings"
- "testing"
- "time"
- )
- type eofReader struct{}
- func (n eofReader) Close() error { return nil }
- func (n eofReader) Read([]byte) (int, error) { return 0, io.EOF }
- type dumpTest struct {
- // Either Req or GetReq can be set/nil but not both.
- Req *http.Request
- GetReq func() *http.Request
- Body any // optional []byte or func() io.ReadCloser to populate Req.Body
- WantDump string
- WantDumpOut string
- MustError bool // if true, the test is expected to throw an error
- NoBody bool // if true, set DumpRequest{,Out} body to false
- }
- var dumpTests = []dumpTest{
- // HTTP/1.1 => chunked coding; body; empty trailer
- {
- Req: &http.Request{
- Method: "GET",
- URL: &url.URL{
- Scheme: "http",
- Host: "www.google.com",
- Path: "/search",
- },
- ProtoMajor: 1,
- ProtoMinor: 1,
- TransferEncoding: []string{"chunked"},
- },
- Body: []byte("abcdef"),
- WantDump: "GET /search HTTP/1.1\r\n" +
- "Host: www.google.com\r\n" +
- "Transfer-Encoding: chunked\r\n\r\n" +
- chunk("abcdef") + chunk(""),
- },
- // Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
- // and doesn't add a User-Agent.
- {
- Req: &http.Request{
- Method: "GET",
- URL: mustParseURL("/foo"),
- ProtoMajor: 1,
- ProtoMinor: 0,
- Header: http.Header{
- "X-Foo": []string{"X-Bar"},
- },
- },
- WantDump: "GET /foo HTTP/1.0\r\n" +
- "X-Foo: X-Bar\r\n\r\n",
- },
- {
- Req: mustNewRequest("GET", "http://example.com/foo", nil),
- WantDumpOut: "GET /foo HTTP/1.1\r\n" +
- "Host: example.com\r\n" +
- "User-Agent: Go-http-client/1.1\r\n" +
- "Accept-Encoding: gzip\r\n\r\n",
- },
- // Test that an https URL doesn't try to do an SSL negotiation
- // with a bytes.Buffer and hang with all goroutines not
- // runnable.
- {
- Req: mustNewRequest("GET", "https://example.com/foo", nil),
- WantDumpOut: "GET /foo HTTP/1.1\r\n" +
- "Host: example.com\r\n" +
- "User-Agent: Go-http-client/1.1\r\n" +
- "Accept-Encoding: gzip\r\n\r\n",
- },
- // Request with Body, but Dump requested without it.
- {
- Req: &http.Request{
- Method: "POST",
- URL: &url.URL{
- Scheme: "http",
- Host: "post.tld",
- Path: "/",
- },
- ContentLength: 6,
- ProtoMajor: 1,
- ProtoMinor: 1,
- },
- Body: []byte("abcdef"),
- WantDumpOut: "POST / HTTP/1.1\r\n" +
- "Host: post.tld\r\n" +
- "User-Agent: Go-http-client/1.1\r\n" +
- "Content-Length: 6\r\n" +
- "Accept-Encoding: gzip\r\n\r\n",
- NoBody: true,
- },
- // Request with Body > 8196 (default buffer size)
- {
- Req: &http.Request{
- Method: "POST",
- URL: &url.URL{
- Scheme: "http",
- Host: "post.tld",
- Path: "/",
- },
- Header: http.Header{
- "Content-Length": []string{"8193"},
- },
- ContentLength: 8193,
- ProtoMajor: 1,
- ProtoMinor: 1,
- },
- Body: bytes.Repeat([]byte("a"), 8193),
- WantDumpOut: "POST / HTTP/1.1\r\n" +
- "Host: post.tld\r\n" +
- "User-Agent: Go-http-client/1.1\r\n" +
- "Content-Length: 8193\r\n" +
- "Accept-Encoding: gzip\r\n\r\n" +
- strings.Repeat("a", 8193),
- WantDump: "POST / HTTP/1.1\r\n" +
- "Host: post.tld\r\n" +
- "Content-Length: 8193\r\n\r\n" +
- strings.Repeat("a", 8193),
- },
- {
- GetReq: func() *http.Request {
- return mustReadRequest("GET http://foo.com/ HTTP/1.1\r\n" +
- "User-Agent: blah\r\n\r\n")
- },
- NoBody: true,
- WantDump: "GET http://foo.com/ HTTP/1.1\r\n" +
- "User-Agent: blah\r\n\r\n",
- },
- // Issue #7215. DumpRequest should return the "Content-Length" when set
- {
- GetReq: func() *http.Request {
- return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
- "Host: passport.myhost.com\r\n" +
- "Content-Length: 3\r\n" +
- "\r\nkey1=name1&key2=name2")
- },
- WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
- "Host: passport.myhost.com\r\n" +
- "Content-Length: 3\r\n" +
- "\r\nkey",
- },
- // Issue #7215. DumpRequest should return the "Content-Length" in ReadRequest
- {
- GetReq: func() *http.Request {
- return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
- "Host: passport.myhost.com\r\n" +
- "Content-Length: 0\r\n" +
- "\r\nkey1=name1&key2=name2")
- },
- WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
- "Host: passport.myhost.com\r\n" +
- "Content-Length: 0\r\n\r\n",
- },
- // Issue #7215. DumpRequest should not return the "Content-Length" if unset
- {
- GetReq: func() *http.Request {
- return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
- "Host: passport.myhost.com\r\n" +
- "\r\nkey1=name1&key2=name2")
- },
- WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
- "Host: passport.myhost.com\r\n\r\n",
- },
- // Issue 18506: make drainBody recognize NoBody. Otherwise
- // this was turning into a chunked request.
- {
- Req: mustNewRequest("POST", "http://example.com/foo", http.NoBody),
- WantDumpOut: "POST /foo HTTP/1.1\r\n" +
- "Host: example.com\r\n" +
- "User-Agent: Go-http-client/1.1\r\n" +
- "Content-Length: 0\r\n" +
- "Accept-Encoding: gzip\r\n\r\n",
- },
- // Issue 34504: a non-nil Body without ContentLength set should be chunked
- {
- Req: &http.Request{
- Method: "PUT",
- URL: &url.URL{
- Scheme: "http",
- Host: "post.tld",
- Path: "/test",
- },
- ContentLength: 0,
- Proto: "HTTP/1.1",
- ProtoMajor: 1,
- ProtoMinor: 1,
- Body: &eofReader{},
- },
- NoBody: true,
- WantDumpOut: "PUT /test HTTP/1.1\r\n" +
- "Host: post.tld\r\n" +
- "User-Agent: Go-http-client/1.1\r\n" +
- "Transfer-Encoding: chunked\r\n" +
- "Accept-Encoding: gzip\r\n\r\n",
- },
- }
- func TestDumpRequest(t *testing.T) {
- // Make a copy of dumpTests and add 10 new cases with an empty URL
- // to test that no goroutines are leaked. See golang.org/issue/32571.
- // 10 seems to be a decent number which always triggers the failure.
- dumpTests := dumpTests[:]
- for i := 0; i < 10; i++ {
- dumpTests = append(dumpTests, dumpTest{
- Req: mustNewRequest("GET", "", nil),
- MustError: true,
- })
- }
- numg0 := runtime.NumGoroutine()
- for i, tt := range dumpTests {
- if tt.Req != nil && tt.GetReq != nil || tt.Req == nil && tt.GetReq == nil {
- t.Errorf("#%d: either .Req(%p) or .GetReq(%p) can be set/nil but not both", i, tt.Req, tt.GetReq)
- continue
- }
- freshReq := func(ti dumpTest) *http.Request {
- req := ti.Req
- if req == nil {
- req = ti.GetReq()
- }
- if req.Header == nil {
- req.Header = make(http.Header)
- }
- if ti.Body == nil {
- return req
- }
- switch b := ti.Body.(type) {
- case []byte:
- req.Body = io.NopCloser(bytes.NewReader(b))
- case func() io.ReadCloser:
- req.Body = b()
- default:
- t.Fatalf("Test %d: unsupported Body of %T", i, ti.Body)
- }
- return req
- }
- if tt.WantDump != "" {
- req := freshReq(tt)
- dump, err := DumpRequest(req, !tt.NoBody)
- if err != nil {
- t.Errorf("DumpRequest #%d: %s\nWantDump:\n%s", i, err, tt.WantDump)
- continue
- }
- if string(dump) != tt.WantDump {
- t.Errorf("DumpRequest %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDump, string(dump))
- continue
- }
- }
- if tt.MustError {
- req := freshReq(tt)
- _, err := DumpRequestOut(req, !tt.NoBody)
- if err == nil {
- t.Errorf("DumpRequestOut #%d: expected an error, got nil", i)
- }
- continue
- }
- if tt.WantDumpOut != "" {
- req := freshReq(tt)
- dump, err := DumpRequestOut(req, !tt.NoBody)
- if err != nil {
- t.Errorf("DumpRequestOut #%d: %s", i, err)
- continue
- }
- if string(dump) != tt.WantDumpOut {
- t.Errorf("DumpRequestOut %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDumpOut, string(dump))
- continue
- }
- }
- }
- // Validate we haven't leaked any goroutines.
- var dg int
- dl := deadline(t, 5*time.Second, time.Second)
- for time.Now().Before(dl) {
- if dg = runtime.NumGoroutine() - numg0; dg <= 4 {
- // No unexpected goroutines.
- return
- }
- // Allow goroutines to schedule and die off.
- runtime.Gosched()
- }
- buf := make([]byte, 4096)
- buf = buf[:runtime.Stack(buf, true)]
- t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf)
- }
- // deadline returns the time which is needed before t.Deadline()
- // if one is configured and it is s greater than needed in the future,
- // otherwise defaultDelay from the current time.
- func deadline(t *testing.T, defaultDelay, needed time.Duration) time.Time {
- if dl, ok := t.Deadline(); ok {
- if dl = dl.Add(-needed); dl.After(time.Now()) {
- // Allow an arbitrarily long delay.
- return dl
- }
- }
- // No deadline configured or its closer than needed from now
- // so just use the default.
- return time.Now().Add(defaultDelay)
- }
- func chunk(s string) string {
- return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
- }
- func mustParseURL(s string) *url.URL {
- u, err := url.Parse(s)
- if err != nil {
- panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
- }
- return u
- }
- func mustNewRequest(method, url string, body io.Reader) *http.Request {
- req, err := http.NewRequest(method, url, body)
- if err != nil {
- panic(fmt.Sprintf("NewRequest(%q, %q, %p) err = %v", method, url, body, err))
- }
- return req
- }
- func mustReadRequest(s string) *http.Request {
- req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(s)))
- if err != nil {
- panic(err)
- }
- return req
- }
- var dumpResTests = []struct {
- res *http.Response
- body bool
- want string
- }{
- {
- res: &http.Response{
- Status: "200 OK",
- StatusCode: 200,
- Proto: "HTTP/1.1",
- ProtoMajor: 1,
- ProtoMinor: 1,
- ContentLength: 50,
- Header: http.Header{
- "Foo": []string{"Bar"},
- },
- Body: io.NopCloser(strings.NewReader("foo")), // shouldn't be used
- },
- body: false, // to verify we see 50, not empty or 3.
- want: `HTTP/1.1 200 OK
- Content-Length: 50
- Foo: Bar`,
- },
- {
- res: &http.Response{
- Status: "200 OK",
- StatusCode: 200,
- Proto: "HTTP/1.1",
- ProtoMajor: 1,
- ProtoMinor: 1,
- ContentLength: 3,
- Body: io.NopCloser(strings.NewReader("foo")),
- },
- body: true,
- want: `HTTP/1.1 200 OK
- Content-Length: 3
- foo`,
- },
- {
- res: &http.Response{
- Status: "200 OK",
- StatusCode: 200,
- Proto: "HTTP/1.1",
- ProtoMajor: 1,
- ProtoMinor: 1,
- ContentLength: -1,
- Body: io.NopCloser(strings.NewReader("foo")),
- TransferEncoding: []string{"chunked"},
- },
- body: true,
- want: `HTTP/1.1 200 OK
- Transfer-Encoding: chunked
- 3
- foo
- 0`,
- },
- {
- res: &http.Response{
- Status: "200 OK",
- StatusCode: 200,
- Proto: "HTTP/1.1",
- ProtoMajor: 1,
- ProtoMinor: 1,
- ContentLength: 0,
- Header: http.Header{
- // To verify if headers are not filtered out.
- "Foo1": []string{"Bar1"},
- "Foo2": []string{"Bar2"},
- },
- Body: nil,
- },
- body: false, // to verify we see 0, not empty.
- want: `HTTP/1.1 200 OK
- Foo1: Bar1
- Foo2: Bar2
- Content-Length: 0`,
- },
- }
- func TestDumpResponse(t *testing.T) {
- for i, tt := range dumpResTests {
- gotb, err := DumpResponse(tt.res, tt.body)
- if err != nil {
- t.Errorf("%d. DumpResponse = %v", i, err)
- continue
- }
- got := string(gotb)
- got = strings.TrimSpace(got)
- got = strings.ReplaceAll(got, "\r", "")
- if got != tt.want {
- t.Errorf("%d.\nDumpResponse got:\n%s\n\nWant:\n%s\n", i, got, tt.want)
- }
- }
- }
- // Issue 38352: Check for deadlock on canceled requests.
- func TestDumpRequestOutIssue38352(t *testing.T) {
- if testing.Short() {
- return
- }
- t.Parallel()
- timeout := 10 * time.Second
- if deadline, ok := t.Deadline(); ok {
- timeout = time.Until(deadline)
- timeout -= time.Second * 2 // Leave 2 seconds to report failures.
- }
- for i := 0; i < 1000; i++ {
- delay := time.Duration(rand.Intn(5)) * time.Millisecond
- ctx, cancel := context.WithTimeout(context.Background(), delay)
- defer cancel()
- r := bytes.NewBuffer(make([]byte, 10000))
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", r)
- if err != nil {
- t.Fatal(err)
- }
- out := make(chan error)
- go func() {
- _, err = DumpRequestOut(req, true)
- out <- err
- }()
- select {
- case <-out:
- case <-time.After(timeout):
- b := &bytes.Buffer{}
- fmt.Fprintf(b, "deadlock detected on iteration %d after %s with delay: %v\n", i, timeout, delay)
- pprof.Lookup("goroutine").WriteTo(b, 1)
- t.Fatal(b.String())
- }
- }
- }
|