net/http: clean up Transport.RoundTrip error handling
If I put a 10 millisecond sleep at testHookWaitResLoop, before the big select in (*persistConn).roundTrip, two flakes immediately started happening, TestTransportBodyReadError (#19231) and TestTransportPersistConnReadLoopEOF. The problem was that there are many ways for a RoundTrip call to fail (errors reading from Request.Body while writing the response, errors writing the response, errors reading the response due to server closes, errors due to servers sending malformed responses, cancelations, timeouts, etc.), and many of those failures then tear down the TCP connection, causing more failures, since there are always at least three goroutines involved (reading, writing, RoundTripping). Because the errors were communicated over buffered channels to a giant select, the error returned to the caller was a function of which random select case was called, which was why a 10ms delay before the select brought out so many bugs. (several fixed in my previous CLs the past few days). Instead, track the error explicitly in the transportRequest, guarded by a mutex. In addition, this CL now: * differentiates between the two ways writing a request can fail: the io.Copy reading from the Request.Body or the io.Copy writing to the network. A new io.Reader type notes read errors from the Request.Body. The read-from-body vs write-to-network errors are now prioritized differently. * unifies the two mapRoundTripErrorFromXXX methods into one mapRoundTripError method since their logic is now the same. * adds a (*Request).WithT(*testing.T) method in export_test.go, usable by tests, to call t.Logf at points during RoundTrip. This is disabled behind a constant except when debugging. * documents and deflakes TestClientRedirectContext I've tested this CL with high -count values, with/without -race, with/without delays before the select, etc. So far it seems robust. Fixes #19231 (TestTransportBodyReadError flake) Updates #14203 (source of errors unclear; they're now tracked more) Updates #15935 (document Transport errors more; at least understood more now) Change-Id: I3cccc3607f369724b5344763e35ad2b7ea415738 Reviewed-on: https://go-review.googlesource.com/37495 Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Josh Bleecher Snyder <josharian@gmail.com>
This commit is contained in:
parent
87649d32ad
commit
9d29be468e
@ -304,6 +304,7 @@ func TestClientRedirects(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests that Client redirects' contexts are derived from the original request's context.
|
||||||
func TestClientRedirectContext(t *testing.T) {
|
func TestClientRedirectContext(t *testing.T) {
|
||||||
setParallel(t)
|
setParallel(t)
|
||||||
defer afterTest(t)
|
defer afterTest(t)
|
||||||
@ -320,10 +321,12 @@ func TestClientRedirectContext(t *testing.T) {
|
|||||||
Transport: tr,
|
Transport: tr,
|
||||||
CheckRedirect: func(req *Request, via []*Request) error {
|
CheckRedirect: func(req *Request, via []*Request) error {
|
||||||
cancel()
|
cancel()
|
||||||
if len(via) > 2 {
|
select {
|
||||||
return errors.New("too many redirects")
|
case <-req.Context().Done():
|
||||||
|
return nil
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
return errors.New("redirected request's context never expired after root request canceled")
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
req, _ := NewRequest("GET", ts.URL, nil)
|
req, _ := NewRequest("GET", ts.URL, nil)
|
||||||
@ -1818,6 +1821,7 @@ func TestTransportBodyReadError(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
req = req.WithT(t)
|
||||||
_, err = tr.RoundTrip(req)
|
_, err = tr.RoundTrip(req)
|
||||||
if err != someErr {
|
if err != someErr {
|
||||||
t.Errorf("Got error: %v; want Request.Body read error: %v", err, someErr)
|
t.Errorf("Got error: %v; want Request.Body read error: %v", err, someErr)
|
||||||
|
@ -8,9 +8,11 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -199,3 +201,7 @@ func (s *Server) ExportAllConnsIdle() bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Request) WithT(t *testing.T) *Request {
|
||||||
|
return r.WithContext(context.WithValue(r.Context(), tLogKey{}, t.Logf))
|
||||||
|
}
|
||||||
|
@ -621,6 +621,9 @@ func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, wai
|
|||||||
// Write body and trailer
|
// Write body and trailer
|
||||||
err = tw.WriteBody(w)
|
err = tw.WriteBody(w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if tw.bodyReadError == err {
|
||||||
|
err = requestBodyReadError{err}
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -630,6 +633,11 @@ func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, wai
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requestBodyReadError wraps an error from (*Request).write to indicate
|
||||||
|
// that the error came from a Read call on the Request.Body.
|
||||||
|
// This error type should not escape the net/http package to users.
|
||||||
|
type requestBodyReadError struct{ error }
|
||||||
|
|
||||||
func idnaASCII(v string) (string, error) {
|
func idnaASCII(v string) (string, error) {
|
||||||
if isASCII(v) {
|
if isASCII(v) {
|
||||||
return v, nil
|
return v, nil
|
||||||
|
@ -51,6 +51,19 @@ func (br *byteReader) Read(p []byte) (n int, err error) {
|
|||||||
return 1, io.EOF
|
return 1, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// transferBodyReader is an io.Reader that reads from tw.Body
|
||||||
|
// and records any non-EOF error in tw.bodyReadError.
|
||||||
|
// It is exactly 1 pointer wide to avoid allocations into interfaces.
|
||||||
|
type transferBodyReader struct{ tw *transferWriter }
|
||||||
|
|
||||||
|
func (br transferBodyReader) Read(p []byte) (n int, err error) {
|
||||||
|
n, err = br.tw.Body.Read(p)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
br.tw.bodyReadError = err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// transferWriter inspects the fields of a user-supplied Request or Response,
|
// transferWriter inspects the fields of a user-supplied Request or Response,
|
||||||
// sanitizes them without changing the user object and provides methods for
|
// sanitizes them without changing the user object and provides methods for
|
||||||
// writing the respective header, body and trailer in wire format.
|
// writing the respective header, body and trailer in wire format.
|
||||||
@ -64,6 +77,7 @@ type transferWriter struct {
|
|||||||
TransferEncoding []string
|
TransferEncoding []string
|
||||||
Trailer Header
|
Trailer Header
|
||||||
IsResponse bool
|
IsResponse bool
|
||||||
|
bodyReadError error // any non-EOF error from reading Body
|
||||||
|
|
||||||
FlushHeaders bool // flush headers to network before body
|
FlushHeaders bool // flush headers to network before body
|
||||||
ByteReadCh chan readResult // non-nil if probeRequestBody called
|
ByteReadCh chan readResult // non-nil if probeRequestBody called
|
||||||
@ -304,24 +318,25 @@ func (t *transferWriter) WriteBody(w io.Writer) error {
|
|||||||
|
|
||||||
// Write body
|
// Write body
|
||||||
if t.Body != nil {
|
if t.Body != nil {
|
||||||
|
var body = transferBodyReader{t}
|
||||||
if chunked(t.TransferEncoding) {
|
if chunked(t.TransferEncoding) {
|
||||||
if bw, ok := w.(*bufio.Writer); ok && !t.IsResponse {
|
if bw, ok := w.(*bufio.Writer); ok && !t.IsResponse {
|
||||||
w = &internal.FlushAfterChunkWriter{Writer: bw}
|
w = &internal.FlushAfterChunkWriter{Writer: bw}
|
||||||
}
|
}
|
||||||
cw := internal.NewChunkedWriter(w)
|
cw := internal.NewChunkedWriter(w)
|
||||||
_, err = io.Copy(cw, t.Body)
|
_, err = io.Copy(cw, body)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = cw.Close()
|
err = cw.Close()
|
||||||
}
|
}
|
||||||
} else if t.ContentLength == -1 {
|
} else if t.ContentLength == -1 {
|
||||||
ncopy, err = io.Copy(w, t.Body)
|
ncopy, err = io.Copy(w, body)
|
||||||
} else {
|
} else {
|
||||||
ncopy, err = io.Copy(w, io.LimitReader(t.Body, t.ContentLength))
|
ncopy, err = io.Copy(w, io.LimitReader(body, t.ContentLength))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var nextra int64
|
var nextra int64
|
||||||
nextra, err = io.Copy(ioutil.Discard, t.Body)
|
nextra, err = io.Copy(ioutil.Discard, body)
|
||||||
ncopy += nextra
|
ncopy += nextra
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -303,11 +303,15 @@ func ProxyURL(fixedURL *url.URL) func(*Request) (*url.URL, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// transportRequest is a wrapper around a *Request that adds
|
// transportRequest is a wrapper around a *Request that adds
|
||||||
// optional extra headers to write.
|
// optional extra headers to write and stores any error to return
|
||||||
|
// from roundTrip.
|
||||||
type transportRequest struct {
|
type transportRequest struct {
|
||||||
*Request // original request, not to be mutated
|
*Request // original request, not to be mutated
|
||||||
extra Header // extra headers to write, or nil
|
extra Header // extra headers to write, or nil
|
||||||
trace *httptrace.ClientTrace // optional
|
trace *httptrace.ClientTrace // optional
|
||||||
|
|
||||||
|
mu sync.Mutex // guards err
|
||||||
|
err error // first setError value for mapRoundTripError to consider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tr *transportRequest) extraHeaders() Header {
|
func (tr *transportRequest) extraHeaders() Header {
|
||||||
@ -317,6 +321,14 @@ func (tr *transportRequest) extraHeaders() Header {
|
|||||||
return tr.extra
|
return tr.extra
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tr *transportRequest) setError(err error) {
|
||||||
|
tr.mu.Lock()
|
||||||
|
if tr.err == nil {
|
||||||
|
tr.err = err
|
||||||
|
}
|
||||||
|
tr.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// RoundTrip implements the RoundTripper interface.
|
// RoundTrip implements the RoundTripper interface.
|
||||||
//
|
//
|
||||||
// For higher-level HTTP client support (such as handling of cookies
|
// For higher-level HTTP client support (such as handling of cookies
|
||||||
@ -1420,22 +1432,41 @@ func (pc *persistConn) closeConnIfStillIdle() {
|
|||||||
pc.close(errIdleConnTimeout)
|
pc.close(errIdleConnTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mapRoundTripErrorFromReadLoop maps the provided readLoop error into
|
// mapRoundTripError returns the appropriate error value for
|
||||||
// the error value that should be returned from persistConn.roundTrip.
|
// persistConn.roundTrip.
|
||||||
|
//
|
||||||
|
// The provided err is the first error that (*persistConn).roundTrip
|
||||||
|
// happened to receive from its select statement.
|
||||||
//
|
//
|
||||||
// The startBytesWritten value should be the value of pc.nwrite before the roundTrip
|
// The startBytesWritten value should be the value of pc.nwrite before the roundTrip
|
||||||
// started writing the request.
|
// started writing the request.
|
||||||
func (pc *persistConn) mapRoundTripErrorFromReadLoop(req *Request, startBytesWritten int64, err error) (out error) {
|
func (pc *persistConn) mapRoundTripError(req *transportRequest, startBytesWritten int64, err error) error {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := pc.canceled(); err != nil {
|
|
||||||
return err
|
// If the request was canceled, that's better than network
|
||||||
|
// failures that were likely the result of tearing down the
|
||||||
|
// connection.
|
||||||
|
if cerr := pc.canceled(); cerr != nil {
|
||||||
|
return cerr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See if an error was set explicitly.
|
||||||
|
req.mu.Lock()
|
||||||
|
reqErr := req.err
|
||||||
|
req.mu.Unlock()
|
||||||
|
if reqErr != nil {
|
||||||
|
return reqErr
|
||||||
|
}
|
||||||
|
|
||||||
if err == errServerClosedIdle {
|
if err == errServerClosedIdle {
|
||||||
|
// Don't decorate
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := err.(transportReadFromServerError); ok {
|
if _, ok := err.(transportReadFromServerError); ok {
|
||||||
|
// Don't decorate
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if pc.isBroken() {
|
if pc.isBroken() {
|
||||||
@ -1443,40 +1474,11 @@ func (pc *persistConn) mapRoundTripErrorFromReadLoop(req *Request, startBytesWri
|
|||||||
if pc.nwrite == startBytesWritten && req.outgoingLength() == 0 {
|
if pc.nwrite == startBytesWritten && req.outgoingLength() == 0 {
|
||||||
return nothingWrittenError{err}
|
return nothingWrittenError{err}
|
||||||
}
|
}
|
||||||
|
return fmt.Errorf("net/http: HTTP/1.x transport connection broken: %v", err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// mapRoundTripErrorAfterClosed returns the error value to be propagated
|
|
||||||
// up to Transport.RoundTrip method when persistConn.roundTrip sees
|
|
||||||
// its pc.closech channel close, indicating the persistConn is dead.
|
|
||||||
// (after closech is closed, pc.closed is valid).
|
|
||||||
func (pc *persistConn) mapRoundTripErrorAfterClosed(req *Request, startBytesWritten int64) error {
|
|
||||||
if err := pc.canceled(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err := pc.closed
|
|
||||||
if err == errServerClosedIdle {
|
|
||||||
// Don't decorate
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, ok := err.(transportReadFromServerError); ok {
|
|
||||||
// Don't decorate
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the writeLoop goroutine to terminated, and then
|
|
||||||
// see if we actually managed to write anything. If not, we
|
|
||||||
// can retry the request.
|
|
||||||
<-pc.writeLoopDone
|
|
||||||
if pc.nwrite == startBytesWritten && req.outgoingLength() == 0 {
|
|
||||||
return nothingWrittenError{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("net/http: HTTP/1.x transport connection broken: %v", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pc *persistConn) readLoop() {
|
func (pc *persistConn) readLoop() {
|
||||||
closeErr := errReadLoopExiting // default value, if not changed below
|
closeErr := errReadLoopExiting // default value, if not changed below
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -1746,6 +1748,17 @@ func (pc *persistConn) writeLoop() {
|
|||||||
case wr := <-pc.writech:
|
case wr := <-pc.writech:
|
||||||
startBytesWritten := pc.nwrite
|
startBytesWritten := pc.nwrite
|
||||||
err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))
|
err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))
|
||||||
|
if bre, ok := err.(requestBodyReadError); ok {
|
||||||
|
err = bre.error
|
||||||
|
// Errors reading from the user's
|
||||||
|
// Request.Body are high priority.
|
||||||
|
// Set it here before sending on the
|
||||||
|
// channels below or calling
|
||||||
|
// pc.close() which tears town
|
||||||
|
// connections and causes other
|
||||||
|
// errors.
|
||||||
|
wr.req.setError(err)
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = pc.bw.Flush()
|
err = pc.bw.Flush()
|
||||||
}
|
}
|
||||||
@ -1913,6 +1926,14 @@ func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err err
|
|||||||
gone := make(chan struct{})
|
gone := make(chan struct{})
|
||||||
defer close(gone)
|
defer close(gone)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
pc.t.setReqCanceler(req.Request, nil)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
const debugRoundTrip = false
|
||||||
|
|
||||||
// Write the request concurrently with waiting for a response,
|
// Write the request concurrently with waiting for a response,
|
||||||
// in case the server decides to reply before reading our full
|
// in case the server decides to reply before reading our full
|
||||||
// request body.
|
// request body.
|
||||||
@ -1929,38 +1950,50 @@ func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err err
|
|||||||
callerGone: gone,
|
callerGone: gone,
|
||||||
}
|
}
|
||||||
|
|
||||||
var re responseAndError
|
|
||||||
var respHeaderTimer <-chan time.Time
|
var respHeaderTimer <-chan time.Time
|
||||||
cancelChan := req.Request.Cancel
|
cancelChan := req.Request.Cancel
|
||||||
ctxDoneChan := req.Context().Done()
|
ctxDoneChan := req.Context().Done()
|
||||||
WaitResponse:
|
|
||||||
for {
|
for {
|
||||||
testHookWaitResLoop()
|
testHookWaitResLoop()
|
||||||
select {
|
select {
|
||||||
case err := <-writeErrCh:
|
case err := <-writeErrCh:
|
||||||
|
if debugRoundTrip {
|
||||||
|
req.logf("writeErrCh resv: %T/%#v", err, err)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if cerr := pc.canceled(); cerr != nil {
|
|
||||||
err = cerr
|
|
||||||
}
|
|
||||||
re = responseAndError{err: err}
|
|
||||||
pc.close(fmt.Errorf("write error: %v", err))
|
pc.close(fmt.Errorf("write error: %v", err))
|
||||||
break WaitResponse
|
return nil, pc.mapRoundTripError(req, startBytesWritten, err)
|
||||||
}
|
}
|
||||||
if d := pc.t.ResponseHeaderTimeout; d > 0 {
|
if d := pc.t.ResponseHeaderTimeout; d > 0 {
|
||||||
|
if debugRoundTrip {
|
||||||
|
req.logf("starting timer for %v", d)
|
||||||
|
}
|
||||||
timer := time.NewTimer(d)
|
timer := time.NewTimer(d)
|
||||||
defer timer.Stop() // prevent leaks
|
defer timer.Stop() // prevent leaks
|
||||||
respHeaderTimer = timer.C
|
respHeaderTimer = timer.C
|
||||||
}
|
}
|
||||||
case <-pc.closech:
|
case <-pc.closech:
|
||||||
re = responseAndError{err: pc.mapRoundTripErrorAfterClosed(req.Request, startBytesWritten)}
|
if debugRoundTrip {
|
||||||
break WaitResponse
|
req.logf("closech recv: %T %#v", pc.closed, pc.closed)
|
||||||
|
}
|
||||||
|
return nil, pc.mapRoundTripError(req, startBytesWritten, pc.closed)
|
||||||
case <-respHeaderTimer:
|
case <-respHeaderTimer:
|
||||||
|
if debugRoundTrip {
|
||||||
|
req.logf("timeout waiting for response headers.")
|
||||||
|
}
|
||||||
pc.close(errTimeout)
|
pc.close(errTimeout)
|
||||||
re = responseAndError{err: errTimeout}
|
return nil, errTimeout
|
||||||
break WaitResponse
|
case re := <-resc:
|
||||||
case re = <-resc:
|
if (re.res == nil) == (re.err == nil) {
|
||||||
re.err = pc.mapRoundTripErrorFromReadLoop(req.Request, startBytesWritten, re.err)
|
panic(fmt.Sprintf("internal error: exactly one of res or err should be set; nil=%v", re.res == nil))
|
||||||
break WaitResponse
|
}
|
||||||
|
if debugRoundTrip {
|
||||||
|
req.logf("resc recv: %p, %T/%#v", re.res, re.err, re.err)
|
||||||
|
}
|
||||||
|
if re.err != nil {
|
||||||
|
return nil, pc.mapRoundTripError(req, startBytesWritten, re.err)
|
||||||
|
}
|
||||||
|
return re.res, nil
|
||||||
case <-cancelChan:
|
case <-cancelChan:
|
||||||
pc.t.CancelRequest(req.Request)
|
pc.t.CancelRequest(req.Request)
|
||||||
cancelChan = nil
|
cancelChan = nil
|
||||||
@ -1970,14 +2003,16 @@ WaitResponse:
|
|||||||
ctxDoneChan = nil
|
ctxDoneChan = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if re.err != nil {
|
// tLogKey is a context WithValue key for test debugging contexts containing
|
||||||
pc.t.setReqCanceler(req.Request, nil)
|
// a t.Logf func. See export_test.go's Request.WithT method.
|
||||||
|
type tLogKey struct{}
|
||||||
|
|
||||||
|
func (r *transportRequest) logf(format string, args ...interface{}) {
|
||||||
|
if logf, ok := r.Request.Context().Value(tLogKey{}).(func(string, ...interface{})); ok {
|
||||||
|
logf(time.Now().Format(time.RFC3339Nano)+": "+format, args...)
|
||||||
}
|
}
|
||||||
if (re.res == nil) == (re.err == nil) {
|
|
||||||
panic("internal error: exactly one of res or err should be set")
|
|
||||||
}
|
|
||||||
return re.res, re.err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// markReused marks this connection as having been successfully used for a
|
// markReused marks this connection as having been successfully used for a
|
||||||
|
@ -30,6 +30,7 @@ func TestTransportPersistConnReadLoopEOF(t *testing.T) {
|
|||||||
|
|
||||||
tr := new(Transport)
|
tr := new(Transport)
|
||||||
req, _ := NewRequest("GET", "http://"+ln.Addr().String(), nil)
|
req, _ := NewRequest("GET", "http://"+ln.Addr().String(), nil)
|
||||||
|
req = req.WithT(t)
|
||||||
treq := &transportRequest{Request: req}
|
treq := &transportRequest{Request: req}
|
||||||
cm := connectMethod{targetScheme: "http", targetAddr: ln.Addr().String()}
|
cm := connectMethod{targetScheme: "http", targetAddr: ln.Addr().String()}
|
||||||
pc, err := tr.getConn(treq, cm)
|
pc, err := tr.getConn(treq, cm)
|
||||||
@ -47,13 +48,13 @@ func TestTransportPersistConnReadLoopEOF(t *testing.T) {
|
|||||||
|
|
||||||
_, err = pc.roundTrip(treq)
|
_, err = pc.roundTrip(treq)
|
||||||
if !isTransportReadFromServerError(err) && err != errServerClosedIdle {
|
if !isTransportReadFromServerError(err) && err != errServerClosedIdle {
|
||||||
t.Fatalf("roundTrip = %#v, %v; want errServerClosedConn or errServerClosedIdle", err, err)
|
t.Errorf("roundTrip = %#v, %v; want errServerClosedIdle or transportReadFromServerError", err, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
<-pc.closech
|
<-pc.closech
|
||||||
err = pc.closed
|
err = pc.closed
|
||||||
if !isTransportReadFromServerError(err) && err != errServerClosedIdle {
|
if !isTransportReadFromServerError(err) && err != errServerClosedIdle {
|
||||||
t.Fatalf("pc.closed = %#v, %v; want errServerClosedConn or errServerClosedIdle", err, err)
|
t.Errorf("pc.closed = %#v, %v; want errServerClosedIdle or transportReadFromServerError", err, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1625,7 +1625,9 @@ func TestTransportResponseHeaderTimeout(t *testing.T) {
|
|||||||
{path: "/fast", want: 200},
|
{path: "/fast", want: 200},
|
||||||
}
|
}
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
res, err := c.Get(ts.URL + tt.path)
|
req, _ := NewRequest("GET", ts.URL+tt.path, nil)
|
||||||
|
req = req.WithT(t)
|
||||||
|
res, err := c.Do(req)
|
||||||
select {
|
select {
|
||||||
case <-inHandler:
|
case <-inHandler:
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user