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