github.com/hxx258456/ccgo@v0.0.5-0.20230213014102-48b35f46f66f/gmhttp/httputil/dump_test.go (about)

     1  // Copyright 2011 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 httputil
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"context"
    11  	"fmt"
    12  	"io"
    13  	"math/rand"
    14  	"net/url"
    15  	"runtime"
    16  	"runtime/pprof"
    17  	"strings"
    18  	"testing"
    19  	"time"
    20  
    21  	http "github.com/hxx258456/ccgo/gmhttp"
    22  )
    23  
    24  type eofReader struct{}
    25  
    26  func (n eofReader) Close() error { return nil }
    27  
    28  func (n eofReader) Read([]byte) (int, error) { return 0, io.EOF }
    29  
    30  type dumpTest struct {
    31  	// Either Req or GetReq can be set/nil but not both.
    32  	Req    *http.Request
    33  	GetReq func() *http.Request
    34  
    35  	Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
    36  
    37  	WantDump    string
    38  	WantDumpOut string
    39  	MustError   bool // if true, the test is expected to throw an error
    40  	NoBody      bool // if true, set DumpRequest{,Out} body to false
    41  }
    42  
    43  var dumpTests = []dumpTest{
    44  	// HTTP/1.1 => chunked coding; body; empty trailer
    45  	{
    46  		Req: &http.Request{
    47  			Method: "GET",
    48  			URL: &url.URL{
    49  				Scheme: "http",
    50  				Host:   "www.google.com",
    51  				Path:   "/search",
    52  			},
    53  			ProtoMajor:       1,
    54  			ProtoMinor:       1,
    55  			TransferEncoding: []string{"chunked"},
    56  		},
    57  
    58  		Body: []byte("abcdef"),
    59  
    60  		WantDump: "GET /search HTTP/1.1\r\n" +
    61  			"Host: www.google.com\r\n" +
    62  			"Transfer-Encoding: chunked\r\n\r\n" +
    63  			chunk("abcdef") + chunk(""),
    64  	},
    65  
    66  	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
    67  	// and doesn't add a User-Agent.
    68  	{
    69  		Req: &http.Request{
    70  			Method:     "GET",
    71  			URL:        mustParseURL("/foo"),
    72  			ProtoMajor: 1,
    73  			ProtoMinor: 0,
    74  			Header: http.Header{
    75  				"X-Foo": []string{"X-Bar"},
    76  			},
    77  		},
    78  
    79  		WantDump: "GET /foo HTTP/1.0\r\n" +
    80  			"X-Foo: X-Bar\r\n\r\n",
    81  	},
    82  
    83  	{
    84  		Req: mustNewRequest("GET", "http://example.com/foo", nil),
    85  
    86  		WantDumpOut: "GET /foo HTTP/1.1\r\n" +
    87  			"Host: example.com\r\n" +
    88  			"User-Agent: Go-http-client/1.1\r\n" +
    89  			"Accept-Encoding: gzip\r\n\r\n",
    90  	},
    91  
    92  	// Test that an https URL doesn't try to do an SSL negotiation
    93  	// with a bytes.Buffer and hang with all goroutines not
    94  	// runnable.
    95  	{
    96  		Req: mustNewRequest("GET", "https://example.com/foo", nil),
    97  		WantDumpOut: "GET /foo HTTP/1.1\r\n" +
    98  			"Host: example.com\r\n" +
    99  			"User-Agent: Go-http-client/1.1\r\n" +
   100  			"Accept-Encoding: gzip\r\n\r\n",
   101  	},
   102  
   103  	// Request with Body, but Dump requested without it.
   104  	{
   105  		Req: &http.Request{
   106  			Method: "POST",
   107  			URL: &url.URL{
   108  				Scheme: "http",
   109  				Host:   "post.tld",
   110  				Path:   "/",
   111  			},
   112  			ContentLength: 6,
   113  			ProtoMajor:    1,
   114  			ProtoMinor:    1,
   115  		},
   116  
   117  		Body: []byte("abcdef"),
   118  
   119  		WantDumpOut: "POST / HTTP/1.1\r\n" +
   120  			"Host: post.tld\r\n" +
   121  			"User-Agent: Go-http-client/1.1\r\n" +
   122  			"Content-Length: 6\r\n" +
   123  			"Accept-Encoding: gzip\r\n\r\n",
   124  
   125  		NoBody: true,
   126  	},
   127  
   128  	// Request with Body > 8196 (default buffer size)
   129  	{
   130  		Req: &http.Request{
   131  			Method: "POST",
   132  			URL: &url.URL{
   133  				Scheme: "http",
   134  				Host:   "post.tld",
   135  				Path:   "/",
   136  			},
   137  			Header: http.Header{
   138  				"Content-Length": []string{"8193"},
   139  			},
   140  
   141  			ContentLength: 8193,
   142  			ProtoMajor:    1,
   143  			ProtoMinor:    1,
   144  		},
   145  
   146  		Body: bytes.Repeat([]byte("a"), 8193),
   147  
   148  		WantDumpOut: "POST / HTTP/1.1\r\n" +
   149  			"Host: post.tld\r\n" +
   150  			"User-Agent: Go-http-client/1.1\r\n" +
   151  			"Content-Length: 8193\r\n" +
   152  			"Accept-Encoding: gzip\r\n\r\n" +
   153  			strings.Repeat("a", 8193),
   154  		WantDump: "POST / HTTP/1.1\r\n" +
   155  			"Host: post.tld\r\n" +
   156  			"Content-Length: 8193\r\n\r\n" +
   157  			strings.Repeat("a", 8193),
   158  	},
   159  
   160  	{
   161  		GetReq: func() *http.Request {
   162  			return mustReadRequest("GET http://foo.com/ HTTP/1.1\r\n" +
   163  				"User-Agent: blah\r\n\r\n")
   164  		},
   165  		NoBody: true,
   166  		WantDump: "GET http://foo.com/ HTTP/1.1\r\n" +
   167  			"User-Agent: blah\r\n\r\n",
   168  	},
   169  
   170  	// Issue #7215. DumpRequest should return the "Content-Length" when set
   171  	{
   172  		GetReq: func() *http.Request {
   173  			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
   174  				"Host: passport.myhost.com\r\n" +
   175  				"Content-Length: 3\r\n" +
   176  				"\r\nkey1=name1&key2=name2")
   177  		},
   178  		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
   179  			"Host: passport.myhost.com\r\n" +
   180  			"Content-Length: 3\r\n" +
   181  			"\r\nkey",
   182  	},
   183  	// Issue #7215. DumpRequest should return the "Content-Length" in ReadRequest
   184  	{
   185  		GetReq: func() *http.Request {
   186  			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
   187  				"Host: passport.myhost.com\r\n" +
   188  				"Content-Length: 0\r\n" +
   189  				"\r\nkey1=name1&key2=name2")
   190  		},
   191  		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
   192  			"Host: passport.myhost.com\r\n" +
   193  			"Content-Length: 0\r\n\r\n",
   194  	},
   195  
   196  	// Issue #7215. DumpRequest should not return the "Content-Length" if unset
   197  	{
   198  		GetReq: func() *http.Request {
   199  			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
   200  				"Host: passport.myhost.com\r\n" +
   201  				"\r\nkey1=name1&key2=name2")
   202  		},
   203  		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
   204  			"Host: passport.myhost.com\r\n\r\n",
   205  	},
   206  
   207  	// Issue 18506: make drainBody recognize NoBody. Otherwise
   208  	// this was turning into a chunked request.
   209  	{
   210  		Req: mustNewRequest("POST", "http://example.com/foo", http.NoBody),
   211  		WantDumpOut: "POST /foo HTTP/1.1\r\n" +
   212  			"Host: example.com\r\n" +
   213  			"User-Agent: Go-http-client/1.1\r\n" +
   214  			"Content-Length: 0\r\n" +
   215  			"Accept-Encoding: gzip\r\n\r\n",
   216  	},
   217  
   218  	// Issue 34504: a non-nil Body without ContentLength set should be chunked
   219  	{
   220  		Req: &http.Request{
   221  			Method: "PUT",
   222  			URL: &url.URL{
   223  				Scheme: "http",
   224  				Host:   "post.tld",
   225  				Path:   "/test",
   226  			},
   227  			ContentLength: 0,
   228  			Proto:         "HTTP/1.1",
   229  			ProtoMajor:    1,
   230  			ProtoMinor:    1,
   231  			Body:          &eofReader{},
   232  		},
   233  		NoBody: true,
   234  		WantDumpOut: "PUT /test HTTP/1.1\r\n" +
   235  			"Host: post.tld\r\n" +
   236  			"User-Agent: Go-http-client/1.1\r\n" +
   237  			"Transfer-Encoding: chunked\r\n" +
   238  			"Accept-Encoding: gzip\r\n\r\n",
   239  	},
   240  }
   241  
   242  func TestDumpRequest(t *testing.T) {
   243  	// Make a copy of dumpTests and add 10 new cases with an empty URL
   244  	// to test that no goroutines are leaked. See golang.org/issue/32571.
   245  	// 10 seems to be a decent number which always triggers the failure.
   246  	dumpTests := dumpTests[:]
   247  	for i := 0; i < 10; i++ {
   248  		dumpTests = append(dumpTests, dumpTest{
   249  			Req:       mustNewRequest("GET", "", nil),
   250  			MustError: true,
   251  		})
   252  	}
   253  	numg0 := runtime.NumGoroutine()
   254  	for i, tt := range dumpTests {
   255  		if tt.Req != nil && tt.GetReq != nil || tt.Req == nil && tt.GetReq == nil {
   256  			t.Errorf("#%d: either .Req(%p) or .GetReq(%p) can be set/nil but not both", i, tt.Req, tt.GetReq)
   257  			continue
   258  		}
   259  
   260  		freshReq := func(ti dumpTest) *http.Request {
   261  			req := ti.Req
   262  			if req == nil {
   263  				req = ti.GetReq()
   264  			}
   265  
   266  			if req.Header == nil {
   267  				req.Header = make(http.Header)
   268  			}
   269  
   270  			if ti.Body == nil {
   271  				return req
   272  			}
   273  			switch b := ti.Body.(type) {
   274  			case []byte:
   275  				req.Body = io.NopCloser(bytes.NewReader(b))
   276  			case func() io.ReadCloser:
   277  				req.Body = b()
   278  			default:
   279  				t.Fatalf("Test %d: unsupported Body of %T", i, ti.Body)
   280  			}
   281  			return req
   282  		}
   283  
   284  		if tt.WantDump != "" {
   285  			req := freshReq(tt)
   286  			dump, err := DumpRequest(req, !tt.NoBody)
   287  			if err != nil {
   288  				t.Errorf("DumpRequest #%d: %s\nWantDump:\n%s", i, err, tt.WantDump)
   289  				continue
   290  			}
   291  			if string(dump) != tt.WantDump {
   292  				t.Errorf("DumpRequest %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDump, string(dump))
   293  				continue
   294  			}
   295  		}
   296  
   297  		if tt.MustError {
   298  			req := freshReq(tt)
   299  			_, err := DumpRequestOut(req, !tt.NoBody)
   300  			if err == nil {
   301  				t.Errorf("DumpRequestOut #%d: expected an error, got nil", i)
   302  			}
   303  			continue
   304  		}
   305  
   306  		if tt.WantDumpOut != "" {
   307  			req := freshReq(tt)
   308  			dump, err := DumpRequestOut(req, !tt.NoBody)
   309  			if err != nil {
   310  				t.Errorf("DumpRequestOut #%d: %s", i, err)
   311  				continue
   312  			}
   313  			if string(dump) != tt.WantDumpOut {
   314  				t.Errorf("DumpRequestOut %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDumpOut, string(dump))
   315  				continue
   316  			}
   317  		}
   318  	}
   319  
   320  	// Validate we haven't leaked any goroutines.
   321  	var dg int
   322  	dl := deadline(t, 5*time.Second, time.Second)
   323  	for time.Now().Before(dl) {
   324  		if dg = runtime.NumGoroutine() - numg0; dg <= 4 {
   325  			// No unexpected goroutines.
   326  			return
   327  		}
   328  
   329  		// Allow goroutines to schedule and die off.
   330  		runtime.Gosched()
   331  	}
   332  
   333  	buf := make([]byte, 4096)
   334  	buf = buf[:runtime.Stack(buf, true)]
   335  	t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf)
   336  }
   337  
   338  // deadline returns the time which is needed before t.Deadline()
   339  // if one is configured and it is s greater than needed in the future,
   340  // otherwise defaultDelay from the current time.
   341  func deadline(t *testing.T, defaultDelay, needed time.Duration) time.Time {
   342  	if dl, ok := t.Deadline(); ok {
   343  		if dl = dl.Add(-needed); dl.After(time.Now()) {
   344  			// Allow an arbitrarily long delay.
   345  			return dl
   346  		}
   347  	}
   348  
   349  	// No deadline configured or its closer than needed from now
   350  	// so just use the default.
   351  	return time.Now().Add(defaultDelay)
   352  }
   353  
   354  func chunk(s string) string {
   355  	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
   356  }
   357  
   358  func mustParseURL(s string) *url.URL {
   359  	u, err := url.Parse(s)
   360  	if err != nil {
   361  		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
   362  	}
   363  	return u
   364  }
   365  
   366  func mustNewRequest(method, url string, body io.Reader) *http.Request {
   367  	req, err := http.NewRequest(method, url, body)
   368  	if err != nil {
   369  		panic(fmt.Sprintf("NewRequest(%q, %q, %p) err = %v", method, url, body, err))
   370  	}
   371  	return req
   372  }
   373  
   374  func mustReadRequest(s string) *http.Request {
   375  	req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(s)))
   376  	if err != nil {
   377  		panic(err)
   378  	}
   379  	return req
   380  }
   381  
   382  var dumpResTests = []struct {
   383  	res  *http.Response
   384  	body bool
   385  	want string
   386  }{
   387  	{
   388  		res: &http.Response{
   389  			Status:        "200 OK",
   390  			StatusCode:    200,
   391  			Proto:         "HTTP/1.1",
   392  			ProtoMajor:    1,
   393  			ProtoMinor:    1,
   394  			ContentLength: 50,
   395  			Header: http.Header{
   396  				"Foo": []string{"Bar"},
   397  			},
   398  			Body: io.NopCloser(strings.NewReader("foo")), // shouldn't be used
   399  		},
   400  		body: false, // to verify we see 50, not empty or 3.
   401  		want: `HTTP/1.1 200 OK
   402  Content-Length: 50
   403  Foo: Bar`,
   404  	},
   405  
   406  	{
   407  		res: &http.Response{
   408  			Status:        "200 OK",
   409  			StatusCode:    200,
   410  			Proto:         "HTTP/1.1",
   411  			ProtoMajor:    1,
   412  			ProtoMinor:    1,
   413  			ContentLength: 3,
   414  			Body:          io.NopCloser(strings.NewReader("foo")),
   415  		},
   416  		body: true,
   417  		want: `HTTP/1.1 200 OK
   418  Content-Length: 3
   419  
   420  foo`,
   421  	},
   422  
   423  	{
   424  		res: &http.Response{
   425  			Status:           "200 OK",
   426  			StatusCode:       200,
   427  			Proto:            "HTTP/1.1",
   428  			ProtoMajor:       1,
   429  			ProtoMinor:       1,
   430  			ContentLength:    -1,
   431  			Body:             io.NopCloser(strings.NewReader("foo")),
   432  			TransferEncoding: []string{"chunked"},
   433  		},
   434  		body: true,
   435  		want: `HTTP/1.1 200 OK
   436  Transfer-Encoding: chunked
   437  
   438  3
   439  foo
   440  0`,
   441  	},
   442  	{
   443  		res: &http.Response{
   444  			Status:        "200 OK",
   445  			StatusCode:    200,
   446  			Proto:         "HTTP/1.1",
   447  			ProtoMajor:    1,
   448  			ProtoMinor:    1,
   449  			ContentLength: 0,
   450  			Header: http.Header{
   451  				// To verify if headers are not filtered out.
   452  				"Foo1": []string{"Bar1"},
   453  				"Foo2": []string{"Bar2"},
   454  			},
   455  			Body: nil,
   456  		},
   457  		body: false, // to verify we see 0, not empty.
   458  		want: `HTTP/1.1 200 OK
   459  Foo1: Bar1
   460  Foo2: Bar2
   461  Content-Length: 0`,
   462  	},
   463  }
   464  
   465  func TestDumpResponse(t *testing.T) {
   466  	for i, tt := range dumpResTests {
   467  		gotb, err := DumpResponse(tt.res, tt.body)
   468  		if err != nil {
   469  			t.Errorf("%d. DumpResponse = %v", i, err)
   470  			continue
   471  		}
   472  		got := string(gotb)
   473  		got = strings.TrimSpace(got)
   474  		got = strings.ReplaceAll(got, "\r", "")
   475  
   476  		if got != tt.want {
   477  			t.Errorf("%d.\nDumpResponse got:\n%s\n\nWant:\n%s\n", i, got, tt.want)
   478  		}
   479  	}
   480  }
   481  
   482  // Issue 38352: Check for deadlock on canceled requests.
   483  func TestDumpRequestOutIssue38352(t *testing.T) {
   484  	if testing.Short() {
   485  		return
   486  	}
   487  	t.Parallel()
   488  
   489  	timeout := 10 * time.Second
   490  	if deadline, ok := t.Deadline(); ok {
   491  		timeout = time.Until(deadline)
   492  		timeout -= time.Second * 2 // Leave 2 seconds to report failures.
   493  	}
   494  	for i := 0; i < 1000; i++ {
   495  		delay := time.Duration(rand.Intn(5)) * time.Millisecond
   496  		ctx, cancel := context.WithTimeout(context.Background(), delay)
   497  		defer cancel()
   498  
   499  		r := bytes.NewBuffer(make([]byte, 10000))
   500  		req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", r)
   501  		if err != nil {
   502  			t.Fatal(err)
   503  		}
   504  
   505  		out := make(chan error)
   506  		go func() {
   507  			_, err = DumpRequestOut(req, true)
   508  			out <- err
   509  		}()
   510  
   511  		select {
   512  		case <-out:
   513  		case <-time.After(timeout):
   514  			b := &bytes.Buffer{}
   515  			fmt.Fprintf(b, "deadlock detected on iteration %d after %s with delay: %v\n", i, timeout, delay)
   516  			pprof.Lookup("goroutine").WriteTo(b, 1)
   517  			t.Fatal(b.String())
   518  		}
   519  	}
   520  }