Source file src/net/http/httputil/dump_test.go

Documentation: net/http/httputil

     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  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"net/http"
    14  	"net/url"
    15  	"runtime"
    16  	"strings"
    17  	"testing"
    18  )
    19  
    20  type dumpTest struct {
    21  	// Either Req or GetReq can be set/nil but not both.
    22  	Req    *http.Request
    23  	GetReq func() *http.Request
    24  
    25  	Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
    26  
    27  	WantDump    string
    28  	WantDumpOut string
    29  	NoBody      bool // if true, set DumpRequest{,Out} body to false
    30  }
    31  
    32  var dumpTests = []dumpTest{
    33  	// HTTP/1.1 => chunked coding; body; empty trailer
    34  	{
    35  		Req: &http.Request{
    36  			Method: "GET",
    37  			URL: &url.URL{
    38  				Scheme: "http",
    39  				Host:   "www.google.com",
    40  				Path:   "/search",
    41  			},
    42  			ProtoMajor:       1,
    43  			ProtoMinor:       1,
    44  			TransferEncoding: []string{"chunked"},
    45  		},
    46  
    47  		Body: []byte("abcdef"),
    48  
    49  		WantDump: "GET /search HTTP/1.1\r\n" +
    50  			"Host: www.google.com\r\n" +
    51  			"Transfer-Encoding: chunked\r\n\r\n" +
    52  			chunk("abcdef") + chunk(""),
    53  	},
    54  
    55  	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
    56  	// and doesn't add a User-Agent.
    57  	{
    58  		Req: &http.Request{
    59  			Method:     "GET",
    60  			URL:        mustParseURL("/foo"),
    61  			ProtoMajor: 1,
    62  			ProtoMinor: 0,
    63  			Header: http.Header{
    64  				"X-Foo": []string{"X-Bar"},
    65  			},
    66  		},
    67  
    68  		WantDump: "GET /foo HTTP/1.0\r\n" +
    69  			"X-Foo: X-Bar\r\n\r\n",
    70  	},
    71  
    72  	{
    73  		Req: mustNewRequest("GET", "http://example.com/foo", nil),
    74  
    75  		WantDumpOut: "GET /foo HTTP/1.1\r\n" +
    76  			"Host: example.com\r\n" +
    77  			"User-Agent: Go-http-client/1.1\r\n" +
    78  			"Accept-Encoding: gzip\r\n\r\n",
    79  	},
    80  
    81  	// Test that an https URL doesn't try to do an SSL negotiation
    82  	// with a bytes.Buffer and hang with all goroutines not
    83  	// runnable.
    84  	{
    85  		Req: mustNewRequest("GET", "https://example.com/foo", nil),
    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  	// Request with Body, but Dump requested without it.
    93  	{
    94  		Req: &http.Request{
    95  			Method: "POST",
    96  			URL: &url.URL{
    97  				Scheme: "http",
    98  				Host:   "post.tld",
    99  				Path:   "/",
   100  			},
   101  			ContentLength: 6,
   102  			ProtoMajor:    1,
   103  			ProtoMinor:    1,
   104  		},
   105  
   106  		Body: []byte("abcdef"),
   107  
   108  		WantDumpOut: "POST / HTTP/1.1\r\n" +
   109  			"Host: post.tld\r\n" +
   110  			"User-Agent: Go-http-client/1.1\r\n" +
   111  			"Content-Length: 6\r\n" +
   112  			"Accept-Encoding: gzip\r\n\r\n",
   113  
   114  		NoBody: true,
   115  	},
   116  
   117  	// Request with Body > 8196 (default buffer size)
   118  	{
   119  		Req: &http.Request{
   120  			Method: "POST",
   121  			URL: &url.URL{
   122  				Scheme: "http",
   123  				Host:   "post.tld",
   124  				Path:   "/",
   125  			},
   126  			Header: http.Header{
   127  				"Content-Length": []string{"8193"},
   128  			},
   129  
   130  			ContentLength: 8193,
   131  			ProtoMajor:    1,
   132  			ProtoMinor:    1,
   133  		},
   134  
   135  		Body: bytes.Repeat([]byte("a"), 8193),
   136  
   137  		WantDumpOut: "POST / HTTP/1.1\r\n" +
   138  			"Host: post.tld\r\n" +
   139  			"User-Agent: Go-http-client/1.1\r\n" +
   140  			"Content-Length: 8193\r\n" +
   141  			"Accept-Encoding: gzip\r\n\r\n" +
   142  			strings.Repeat("a", 8193),
   143  		WantDump: "POST / HTTP/1.1\r\n" +
   144  			"Host: post.tld\r\n" +
   145  			"Content-Length: 8193\r\n\r\n" +
   146  			strings.Repeat("a", 8193),
   147  	},
   148  
   149  	{
   150  		GetReq: func() *http.Request {
   151  			return mustReadRequest("GET http://foo.com/ HTTP/1.1\r\n" +
   152  				"User-Agent: blah\r\n\r\n")
   153  		},
   154  		NoBody: true,
   155  		WantDump: "GET http://foo.com/ HTTP/1.1\r\n" +
   156  			"User-Agent: blah\r\n\r\n",
   157  	},
   158  
   159  	// Issue #7215. DumpRequest should return the "Content-Length" when set
   160  	{
   161  		GetReq: func() *http.Request {
   162  			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
   163  				"Host: passport.myhost.com\r\n" +
   164  				"Content-Length: 3\r\n" +
   165  				"\r\nkey1=name1&key2=name2")
   166  		},
   167  		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
   168  			"Host: passport.myhost.com\r\n" +
   169  			"Content-Length: 3\r\n" +
   170  			"\r\nkey",
   171  	},
   172  	// Issue #7215. DumpRequest should return the "Content-Length" in ReadRequest
   173  	{
   174  		GetReq: func() *http.Request {
   175  			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
   176  				"Host: passport.myhost.com\r\n" +
   177  				"Content-Length: 0\r\n" +
   178  				"\r\nkey1=name1&key2=name2")
   179  		},
   180  		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
   181  			"Host: passport.myhost.com\r\n" +
   182  			"Content-Length: 0\r\n\r\n",
   183  	},
   184  
   185  	// Issue #7215. DumpRequest should not return the "Content-Length" if unset
   186  	{
   187  		GetReq: func() *http.Request {
   188  			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
   189  				"Host: passport.myhost.com\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\r\n",
   194  	},
   195  
   196  	// Issue 18506: make drainBody recognize NoBody. Otherwise
   197  	// this was turning into a chunked request.
   198  	{
   199  		Req: mustNewRequest("POST", "http://example.com/foo", http.NoBody),
   200  		WantDumpOut: "POST /foo HTTP/1.1\r\n" +
   201  			"Host: example.com\r\n" +
   202  			"User-Agent: Go-http-client/1.1\r\n" +
   203  			"Content-Length: 0\r\n" +
   204  			"Accept-Encoding: gzip\r\n\r\n",
   205  	},
   206  }
   207  
   208  func TestDumpRequest(t *testing.T) {
   209  	numg0 := runtime.NumGoroutine()
   210  	for i, tt := range dumpTests {
   211  		if tt.Req != nil && tt.GetReq != nil || tt.Req == nil && tt.GetReq == nil {
   212  			t.Errorf("#%d: either .Req(%p) or .GetReq(%p) can be set/nil but not both", i, tt.Req, tt.GetReq)
   213  			continue
   214  		}
   215  
   216  		freshReq := func(ti dumpTest) *http.Request {
   217  			req := ti.Req
   218  			if req == nil {
   219  				req = ti.GetReq()
   220  			}
   221  
   222  			if req.Header == nil {
   223  				req.Header = make(http.Header)
   224  			}
   225  
   226  			if ti.Body == nil {
   227  				return req
   228  			}
   229  			switch b := ti.Body.(type) {
   230  			case []byte:
   231  				req.Body = ioutil.NopCloser(bytes.NewReader(b))
   232  			case func() io.ReadCloser:
   233  				req.Body = b()
   234  			default:
   235  				t.Fatalf("Test %d: unsupported Body of %T", i, ti.Body)
   236  			}
   237  			return req
   238  		}
   239  
   240  		if tt.WantDump != "" {
   241  			req := freshReq(tt)
   242  			dump, err := DumpRequest(req, !tt.NoBody)
   243  			if err != nil {
   244  				t.Errorf("DumpRequest #%d: %s\nWantDump:\n%s", i, err, tt.WantDump)
   245  				continue
   246  			}
   247  			if string(dump) != tt.WantDump {
   248  				t.Errorf("DumpRequest %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDump, string(dump))
   249  				continue
   250  			}
   251  		}
   252  
   253  		if tt.WantDumpOut != "" {
   254  			req := freshReq(tt)
   255  			dump, err := DumpRequestOut(req, !tt.NoBody)
   256  			if err != nil {
   257  				t.Errorf("DumpRequestOut #%d: %s", i, err)
   258  				continue
   259  			}
   260  			if string(dump) != tt.WantDumpOut {
   261  				t.Errorf("DumpRequestOut %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDumpOut, string(dump))
   262  				continue
   263  			}
   264  		}
   265  	}
   266  	if dg := runtime.NumGoroutine() - numg0; dg > 4 {
   267  		buf := make([]byte, 4096)
   268  		buf = buf[:runtime.Stack(buf, true)]
   269  		t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf)
   270  	}
   271  }
   272  
   273  func chunk(s string) string {
   274  	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
   275  }
   276  
   277  func mustParseURL(s string) *url.URL {
   278  	u, err := url.Parse(s)
   279  	if err != nil {
   280  		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
   281  	}
   282  	return u
   283  }
   284  
   285  func mustNewRequest(method, url string, body io.Reader) *http.Request {
   286  	req, err := http.NewRequest(method, url, body)
   287  	if err != nil {
   288  		panic(fmt.Sprintf("NewRequest(%q, %q, %p) err = %v", method, url, body, err))
   289  	}
   290  	return req
   291  }
   292  
   293  func mustReadRequest(s string) *http.Request {
   294  	req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(s)))
   295  	if err != nil {
   296  		panic(err)
   297  	}
   298  	return req
   299  }
   300  
   301  var dumpResTests = []struct {
   302  	res  *http.Response
   303  	body bool
   304  	want string
   305  }{
   306  	{
   307  		res: &http.Response{
   308  			Status:        "200 OK",
   309  			StatusCode:    200,
   310  			Proto:         "HTTP/1.1",
   311  			ProtoMajor:    1,
   312  			ProtoMinor:    1,
   313  			ContentLength: 50,
   314  			Header: http.Header{
   315  				"Foo": []string{"Bar"},
   316  			},
   317  			Body: ioutil.NopCloser(strings.NewReader("foo")), // shouldn't be used
   318  		},
   319  		body: false, // to verify we see 50, not empty or 3.
   320  		want: `HTTP/1.1 200 OK
   321  Content-Length: 50
   322  Foo: Bar`,
   323  	},
   324  
   325  	{
   326  		res: &http.Response{
   327  			Status:        "200 OK",
   328  			StatusCode:    200,
   329  			Proto:         "HTTP/1.1",
   330  			ProtoMajor:    1,
   331  			ProtoMinor:    1,
   332  			ContentLength: 3,
   333  			Body:          ioutil.NopCloser(strings.NewReader("foo")),
   334  		},
   335  		body: true,
   336  		want: `HTTP/1.1 200 OK
   337  Content-Length: 3
   338  
   339  foo`,
   340  	},
   341  
   342  	{
   343  		res: &http.Response{
   344  			Status:           "200 OK",
   345  			StatusCode:       200,
   346  			Proto:            "HTTP/1.1",
   347  			ProtoMajor:       1,
   348  			ProtoMinor:       1,
   349  			ContentLength:    -1,
   350  			Body:             ioutil.NopCloser(strings.NewReader("foo")),
   351  			TransferEncoding: []string{"chunked"},
   352  		},
   353  		body: true,
   354  		want: `HTTP/1.1 200 OK
   355  Transfer-Encoding: chunked
   356  
   357  3
   358  foo
   359  0`,
   360  	},
   361  	{
   362  		res: &http.Response{
   363  			Status:        "200 OK",
   364  			StatusCode:    200,
   365  			Proto:         "HTTP/1.1",
   366  			ProtoMajor:    1,
   367  			ProtoMinor:    1,
   368  			ContentLength: 0,
   369  			Header: http.Header{
   370  				// To verify if headers are not filtered out.
   371  				"Foo1": []string{"Bar1"},
   372  				"Foo2": []string{"Bar2"},
   373  			},
   374  			Body: nil,
   375  		},
   376  		body: false, // to verify we see 0, not empty.
   377  		want: `HTTP/1.1 200 OK
   378  Foo1: Bar1
   379  Foo2: Bar2
   380  Content-Length: 0`,
   381  	},
   382  }
   383  
   384  func TestDumpResponse(t *testing.T) {
   385  	for i, tt := range dumpResTests {
   386  		gotb, err := DumpResponse(tt.res, tt.body)
   387  		if err != nil {
   388  			t.Errorf("%d. DumpResponse = %v", i, err)
   389  			continue
   390  		}
   391  		got := string(gotb)
   392  		got = strings.TrimSpace(got)
   393  		got = strings.ReplaceAll(got, "\r", "")
   394  
   395  		if got != tt.want {
   396  			t.Errorf("%d.\nDumpResponse got:\n%s\n\nWant:\n%s\n", i, got, tt.want)
   397  		}
   398  	}
   399  }
   400  

View as plain text