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 }