@@ -2601,86 +2601,160 @@ type writerFuncConn struct {
2601
2601
2602
2602
func (c writerFuncConn ) Write (p []byte ) (n int , err error ) { return c .write (p ) }
2603
2603
2604
- // Issue 4677. If we try to reuse a connection that the server is in the
2605
- // process of closing, we may end up successfully writing out our request (or a
2606
- // portion of our request) only to find a connection error when we try to read
2607
- // from (or finish writing to) the socket.
2604
+ // Issues 4677, 18241, and 17844 . If we try to reuse a connection that the
2605
+ // server is in the process of closing, we may end up successfully writing out
2606
+ // our request (or a portion of our request) only to find a connection error
2607
+ // when we try to read from (or finish writing to) the socket.
2608
2608
//
2609
- // NOTE: we resend a request only if the request is idempotent, we reused a
2610
- // keep-alive connection, and we haven't yet received any header data. This
2611
- // automatically prevents an infinite resend loop because we'll run out of the
2612
- // cached keep-alive connections eventually.
2613
- func TestRetryIdempotentRequestsOnError (t * testing.T ) {
2614
- defer afterTest (t )
2609
+ // NOTE: we resend a request only if:
2610
+ // - we reused a keep-alive connection
2611
+ // - we haven't yet received any header data
2612
+ // - either we wrote no bytes to the server, or the request is idempotent
2613
+ // This automatically prevents an infinite resend loop because we'll run out of
2614
+ // the cached keep-alive connections eventually.
2615
+ func TestRetryRequestsOnError (t * testing.T ) {
2616
+ newRequest := func (method , urlStr string , body io.Reader ) * Request {
2617
+ req , err := NewRequest (method , urlStr , body )
2618
+ if err != nil {
2619
+ t .Fatal (err )
2620
+ }
2621
+ return req
2622
+ }
2615
2623
2616
- var (
2617
- mu sync.Mutex
2618
- logbuf bytes.Buffer
2619
- )
2620
- logf := func (format string , args ... interface {}) {
2621
- mu .Lock ()
2622
- defer mu .Unlock ()
2623
- fmt .Fprintf (& logbuf , format , args ... )
2624
- logbuf .WriteByte ('\n' )
2624
+ testCases := []struct {
2625
+ name string
2626
+ failureN int
2627
+ failureErr error
2628
+ // Note that we can't just re-use the Request object across calls to c.Do
2629
+ // because we need to rewind Body between calls. (GetBody is only used to
2630
+ // rewind Body on failure and redirects, not just because it's done.)
2631
+ req func () * Request
2632
+ reqString string
2633
+ }{
2634
+ {
2635
+ name : "IdempotentNoBodySomeWritten" ,
2636
+ // Believe that we've written some bytes to the server, so we know we're
2637
+ // not just in the "retry when no bytes sent" case".
2638
+ failureN : 1 ,
2639
+ // Use the specific error that shouldRetryRequest looks for with idempotent requests.
2640
+ failureErr : ExportErrServerClosedIdle ,
2641
+ req : func () * Request {
2642
+ return newRequest ("GET" , "http://fake.golang" , nil )
2643
+ },
2644
+ reqString : `GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n` ,
2645
+ },
2646
+ {
2647
+ name : "IdempotentGetBodySomeWritten" ,
2648
+ // Believe that we've written some bytes to the server, so we know we're
2649
+ // not just in the "retry when no bytes sent" case".
2650
+ failureN : 1 ,
2651
+ // Use the specific error that shouldRetryRequest looks for with idempotent requests.
2652
+ failureErr : ExportErrServerClosedIdle ,
2653
+ req : func () * Request {
2654
+ return newRequest ("GET" , "http://fake.golang" , strings .NewReader ("foo\n " ))
2655
+ },
2656
+ reqString : `GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nContent-Length: 4\r\nAccept-Encoding: gzip\r\n\r\nfoo\n` ,
2657
+ },
2658
+ {
2659
+ name : "NothingWrittenNoBody" ,
2660
+ // It's key that we return 0 here -- that's what enables Transport to know
2661
+ // that nothing was written, even though this is a non-idempotent request.
2662
+ failureN : 0 ,
2663
+ failureErr : errors .New ("second write fails" ),
2664
+ req : func () * Request {
2665
+ return newRequest ("DELETE" , "http://fake.golang" , nil )
2666
+ },
2667
+ reqString : `DELETE / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n` ,
2668
+ },
2669
+ {
2670
+ name : "NothingWrittenGetBody" ,
2671
+ // It's key that we return 0 here -- that's what enables Transport to know
2672
+ // that nothing was written, even though this is a non-idempotent request.
2673
+ failureN : 0 ,
2674
+ failureErr : errors .New ("second write fails" ),
2675
+ // Note that NewRequest will set up GetBody for strings.Reader, which is
2676
+ // required for the retry to occur
2677
+ req : func () * Request {
2678
+ return newRequest ("POST" , "http://fake.golang" , strings .NewReader ("foo\n " ))
2679
+ },
2680
+ reqString : `POST / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nContent-Length: 4\r\nAccept-Encoding: gzip\r\n\r\nfoo\n` ,
2681
+ },
2625
2682
}
2626
2683
2627
- ts := httptest .NewServer (HandlerFunc (func (w ResponseWriter , r * Request ) {
2628
- logf ("Handler" )
2629
- w .Header ().Set ("X-Status" , "ok" )
2630
- }))
2631
- defer ts .Close ()
2684
+ for _ , tc := range testCases {
2685
+ t .Run (tc .name , func (t * testing.T ) {
2686
+ defer afterTest (t )
2632
2687
2633
- var writeNumAtomic int32
2634
- c := ts .Client ()
2635
- c .Transport .(* Transport ).Dial = func (network , addr string ) (net.Conn , error ) {
2636
- logf ("Dial" )
2637
- c , err := net .Dial (network , ts .Listener .Addr ().String ())
2638
- if err != nil {
2639
- logf ("Dial error: %v" , err )
2640
- return nil , err
2641
- }
2642
- return & writerFuncConn {
2643
- Conn : c ,
2644
- write : func (p []byte ) (n int , err error ) {
2645
- if atomic .AddInt32 (& writeNumAtomic , 1 ) == 2 {
2646
- logf ("intentional write failure" )
2647
- return 0 , errors .New ("second write fails" )
2688
+ var (
2689
+ mu sync.Mutex
2690
+ logbuf bytes.Buffer
2691
+ )
2692
+ logf := func (format string , args ... interface {}) {
2693
+ mu .Lock ()
2694
+ defer mu .Unlock ()
2695
+ fmt .Fprintf (& logbuf , format , args ... )
2696
+ logbuf .WriteByte ('\n' )
2697
+ }
2698
+
2699
+ ts := httptest .NewServer (HandlerFunc (func (w ResponseWriter , r * Request ) {
2700
+ logf ("Handler" )
2701
+ w .Header ().Set ("X-Status" , "ok" )
2702
+ }))
2703
+ defer ts .Close ()
2704
+
2705
+ var writeNumAtomic int32
2706
+ c := ts .Client ()
2707
+ c .Transport .(* Transport ).Dial = func (network , addr string ) (net.Conn , error ) {
2708
+ logf ("Dial" )
2709
+ c , err := net .Dial (network , ts .Listener .Addr ().String ())
2710
+ if err != nil {
2711
+ logf ("Dial error: %v" , err )
2712
+ return nil , err
2648
2713
}
2649
- logf ("Write(%q)" , p )
2650
- return c .Write (p )
2651
- },
2652
- }, nil
2653
- }
2714
+ return & writerFuncConn {
2715
+ Conn : c ,
2716
+ write : func (p []byte ) (n int , err error ) {
2717
+ if atomic .AddInt32 (& writeNumAtomic , 1 ) == 2 {
2718
+ logf ("intentional write failure" )
2719
+ return tc .failureN , tc .failureErr
2720
+ }
2721
+ logf ("Write(%q)" , p )
2722
+ return c .Write (p )
2723
+ },
2724
+ }, nil
2725
+ }
2654
2726
2655
- SetRoundTripRetried (func () {
2656
- logf ("Retried." )
2657
- })
2658
- defer SetRoundTripRetried (nil )
2727
+ SetRoundTripRetried (func () {
2728
+ logf ("Retried." )
2729
+ })
2730
+ defer SetRoundTripRetried (nil )
2659
2731
2660
- for i := 0 ; i < 3 ; i ++ {
2661
- res , err := c .Get ( "http://fake.golang/" )
2662
- if err != nil {
2663
- t .Fatalf ("i=%d: Get = %v" , i , err )
2664
- }
2665
- res .Body .Close ()
2666
- }
2732
+ for i := 0 ; i < 3 ; i ++ {
2733
+ res , err := c .Do ( tc . req () )
2734
+ if err != nil {
2735
+ t .Fatalf ("i=%d: Do = %v" , i , err )
2736
+ }
2737
+ res .Body .Close ()
2738
+ }
2667
2739
2668
- mu .Lock ()
2669
- got := logbuf .String ()
2670
- mu .Unlock ()
2671
- const want = `Dial
2672
- Write("GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n ")
2740
+ mu .Lock ()
2741
+ got := logbuf .String ()
2742
+ mu .Unlock ()
2743
+ want := fmt . Sprintf ( `Dial
2744
+ Write("%s ")
2673
2745
Handler
2674
2746
intentional write failure
2675
2747
Retried.
2676
2748
Dial
2677
- Write("GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n ")
2749
+ Write("%s ")
2678
2750
Handler
2679
- Write("GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n ")
2751
+ Write("%s ")
2680
2752
Handler
2681
- `
2682
- if got != want {
2683
- t .Errorf ("Log of events differs. Got:\n %s\n Want:\n %s" , got , want )
2753
+ ` , tc .reqString , tc .reqString , tc .reqString )
2754
+ if got != want {
2755
+ t .Errorf ("Log of events differs. Got:\n %s\n Want:\n %s" , got , want )
2756
+ }
2757
+ })
2684
2758
}
2685
2759
}
2686
2760
0 commit comments