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 }