mirror of
https://github.com/golang/go.git
synced 2025-05-16 21:04:38 +00:00
This is another attempt at the change attempted in https://golang.org/cl/27117 and rolled back in https://golang.org/cl/34134 The difference between this and the previous attempt is that this version only retries if the new field GetBody is set on the Request. Additionally, this allows retries of requests with idempotent methods even if they have bodies, as long as GetBody is defined. This also fixes an existing bug where readLoop could make a redundant call to setReqCanceler for DELETE/POST/PUT/etc requests with no body with zero bytes written. This clarifies the existing TestRetryIdempotentRequestsOnError test (and changes it into a test with 4 subtests). When that test was written, it was in fact testing "retry idempotent requests" logic, but the logic had changed since then, and it was actually testing "retry requests with no body when no bytes have been written". (You can confirm this by changing the existing test from a GET to a DELETE; it passes without the changes in this CL.) We now test for the no-Body and GetBody cases for both idempotent and nothing-written-non-idempotent requests. Fixes #18241 Fixes #17844 Change-Id: I69a48691796f6dc08c31f7aa7887b7dfd67e278a Reviewed-on: https://go-review.googlesource.com/42142 Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
169 lines
3.7 KiB
Go
169 lines
3.7 KiB
Go
// Copyright 2016 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// White-box tests for transport.go (in package http instead of http_test).
|
|
|
|
package http
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Issue 15446: incorrect wrapping of errors when server closes an idle connection.
|
|
func TestTransportPersistConnReadLoopEOF(t *testing.T) {
|
|
ln := newLocalListener(t)
|
|
defer ln.Close()
|
|
|
|
connc := make(chan net.Conn, 1)
|
|
go func() {
|
|
defer close(connc)
|
|
c, err := ln.Accept()
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
connc <- c
|
|
}()
|
|
|
|
tr := new(Transport)
|
|
req, _ := NewRequest("GET", "http://"+ln.Addr().String(), nil)
|
|
req = req.WithT(t)
|
|
treq := &transportRequest{Request: req}
|
|
cm := connectMethod{targetScheme: "http", targetAddr: ln.Addr().String()}
|
|
pc, err := tr.getConn(treq, cm)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer pc.close(errors.New("test over"))
|
|
|
|
conn := <-connc
|
|
if conn == nil {
|
|
// Already called t.Error in the accept goroutine.
|
|
return
|
|
}
|
|
conn.Close() // simulate the server hanging up on the client
|
|
|
|
_, err = pc.roundTrip(treq)
|
|
if !isTransportReadFromServerError(err) && err != errServerClosedIdle {
|
|
t.Errorf("roundTrip = %#v, %v; want errServerClosedIdle or transportReadFromServerError", err, err)
|
|
}
|
|
|
|
<-pc.closech
|
|
err = pc.closed
|
|
if !isTransportReadFromServerError(err) && err != errServerClosedIdle {
|
|
t.Errorf("pc.closed = %#v, %v; want errServerClosedIdle or transportReadFromServerError", err, err)
|
|
}
|
|
}
|
|
|
|
func isTransportReadFromServerError(err error) bool {
|
|
_, ok := err.(transportReadFromServerError)
|
|
return ok
|
|
}
|
|
|
|
func newLocalListener(t *testing.T) net.Listener {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
ln, err = net.Listen("tcp6", "[::1]:0")
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return ln
|
|
}
|
|
|
|
func dummyRequest(method string) *Request {
|
|
req, err := NewRequest(method, "http://fake.tld/", nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return req
|
|
}
|
|
func dummyRequestWithBody(method string) *Request {
|
|
req, err := NewRequest(method, "http://fake.tld/", strings.NewReader("foo"))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return req
|
|
}
|
|
|
|
func dummyRequestWithBodyNoGetBody(method string) *Request {
|
|
req := dummyRequestWithBody(method)
|
|
req.GetBody = nil
|
|
return req
|
|
}
|
|
|
|
func TestTransportShouldRetryRequest(t *testing.T) {
|
|
tests := []struct {
|
|
pc *persistConn
|
|
req *Request
|
|
|
|
err error
|
|
want bool
|
|
}{
|
|
0: {
|
|
pc: &persistConn{reused: false},
|
|
req: dummyRequest("POST"),
|
|
err: nothingWrittenError{},
|
|
want: false,
|
|
},
|
|
1: {
|
|
pc: &persistConn{reused: true},
|
|
req: dummyRequest("POST"),
|
|
err: nothingWrittenError{},
|
|
want: true,
|
|
},
|
|
2: {
|
|
pc: &persistConn{reused: true},
|
|
req: dummyRequest("POST"),
|
|
err: http2ErrNoCachedConn,
|
|
want: true,
|
|
},
|
|
3: {
|
|
pc: &persistConn{reused: true},
|
|
req: dummyRequest("POST"),
|
|
err: errMissingHost,
|
|
want: false,
|
|
},
|
|
4: {
|
|
pc: &persistConn{reused: true},
|
|
req: dummyRequest("POST"),
|
|
err: transportReadFromServerError{},
|
|
want: false,
|
|
},
|
|
5: {
|
|
pc: &persistConn{reused: true},
|
|
req: dummyRequest("GET"),
|
|
err: transportReadFromServerError{},
|
|
want: true,
|
|
},
|
|
6: {
|
|
pc: &persistConn{reused: true},
|
|
req: dummyRequest("GET"),
|
|
err: errServerClosedIdle,
|
|
want: true,
|
|
},
|
|
7: {
|
|
pc: &persistConn{reused: true},
|
|
req: dummyRequestWithBody("POST"),
|
|
err: nothingWrittenError{},
|
|
want: true,
|
|
},
|
|
8: {
|
|
pc: &persistConn{reused: true},
|
|
req: dummyRequestWithBodyNoGetBody("POST"),
|
|
err: nothingWrittenError{},
|
|
want: false,
|
|
},
|
|
}
|
|
for i, tt := range tests {
|
|
got := tt.pc.shouldRetryRequest(tt.req, tt.err)
|
|
if got != tt.want {
|
|
t.Errorf("%d. shouldRetryRequest = %v; want %v", i, got, tt.want)
|
|
}
|
|
}
|
|
}
|