Skip to content

Commit a5aa91b

Browse files
committed
net/http: make client await response concurrently with writing request
If the server replies with an HTTP response before we're done writing our body (for instance "401 Unauthorized" response), we were previously ignoring that, since we returned our write error ("broken pipe", etc) before ever reading the response. Now we read and write at the same time. Fixes #3595 R=rsc, adg CC=golang-dev https://golang.org/cl/6238043
1 parent e1d9fcd commit a5aa91b

File tree

2 files changed

+86
-10
lines changed

2 files changed

+86
-10
lines changed

src/pkg/net/http/transport.go

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ func (t *Transport) getConn(cm *connectMethod) (*persistConn, error) {
323323
cacheKey: cm.String(),
324324
conn: conn,
325325
reqch: make(chan requestAndChan, 50),
326+
writech: make(chan writeRequest, 50),
326327
}
327328

328329
switch {
@@ -380,6 +381,7 @@ func (t *Transport) getConn(cm *connectMethod) (*persistConn, error) {
380381
pconn.br = bufio.NewReader(pconn.conn)
381382
pconn.bw = bufio.NewWriter(pconn.conn)
382383
go pconn.readLoop()
384+
go pconn.writeLoop()
383385
return pconn, nil
384386
}
385387

@@ -487,7 +489,8 @@ type persistConn struct {
487489
closed bool // whether conn has been closed
488490
br *bufio.Reader // from conn
489491
bw *bufio.Writer // to conn
490-
reqch chan requestAndChan // written by roundTrip(); read by readLoop()
492+
reqch chan requestAndChan // written by roundTrip; read by readLoop
493+
writech chan writeRequest // written by roundTrip; read by writeLoop
491494
isProxy bool
492495

493496
// mutateHeaderFunc is an optional func to modify extra
@@ -519,6 +522,7 @@ func remoteSideClosed(err error) bool {
519522
}
520523

521524
func (pc *persistConn) readLoop() {
525+
defer close(pc.writech)
522526
alive := true
523527
var lastbody io.ReadCloser // last response body, if any, read on this connection
524528

@@ -579,7 +583,7 @@ func (pc *persistConn) readLoop() {
579583
if alive && !pc.t.putIdleConn(pc) {
580584
alive = false
581585
}
582-
if !alive {
586+
if !alive || pc.isBroken() {
583587
pc.close()
584588
}
585589
waitForBodyRead <- true
@@ -615,6 +619,23 @@ func (pc *persistConn) readLoop() {
615619
}
616620
}
617621

622+
func (pc *persistConn) writeLoop() {
623+
for wr := range pc.writech {
624+
if pc.isBroken() {
625+
wr.ch <- errors.New("http: can't write HTTP request on broken connection")
626+
continue
627+
}
628+
err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra)
629+
if err == nil {
630+
err = pc.bw.Flush()
631+
}
632+
if err != nil {
633+
pc.markBroken()
634+
}
635+
wr.ch <- err
636+
}
637+
}
638+
618639
type responseAndError struct {
619640
res *Response
620641
err error
@@ -630,6 +651,15 @@ type requestAndChan struct {
630651
addedGzip bool
631652
}
632653

654+
// A writeRequest is sent by the readLoop's goroutine to the
655+
// writeLoop's goroutine to write a request while the read loop
656+
// concurrently waits on both the write response and the server's
657+
// reply.
658+
type writeRequest struct {
659+
req *transportRequest
660+
ch chan<- error
661+
}
662+
633663
func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
634664
if pc.mutateHeaderFunc != nil {
635665
pc.mutateHeaderFunc(req.extraHeaders())
@@ -652,23 +682,45 @@ func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err err
652682
pc.numExpectedResponses++
653683
pc.lk.Unlock()
654684

655-
err = req.Request.write(pc.bw, pc.isProxy, req.extra)
656-
if err != nil {
657-
pc.close()
658-
return
685+
// Write the request concurrently with waiting for a response,
686+
// in case the server decides to reply before reading our full
687+
// request body.
688+
writeErrCh := make(chan error, 1)
689+
pc.writech <- writeRequest{req, writeErrCh}
690+
691+
resc := make(chan responseAndError, 1)
692+
pc.reqch <- requestAndChan{req.Request, resc, requestedGzip}
693+
694+
var re responseAndError
695+
WaitResponse:
696+
for {
697+
select {
698+
case err := <-writeErrCh:
699+
if err != nil {
700+
re = responseAndError{nil, err}
701+
break WaitResponse
702+
}
703+
case re = <-resc:
704+
break WaitResponse
705+
}
659706
}
660-
pc.bw.Flush()
661707

662-
ch := make(chan responseAndError, 1)
663-
pc.reqch <- requestAndChan{req.Request, ch, requestedGzip}
664-
re := <-ch
665708
pc.lk.Lock()
666709
pc.numExpectedResponses--
667710
pc.lk.Unlock()
668711

669712
return re.res, re.err
670713
}
671714

715+
// markBroken marks a connection as broken (so it's not reused).
716+
// It differs from close in that it doesn't close the underlying
717+
// connection for use when it's still being read.
718+
func (pc *persistConn) markBroken() {
719+
pc.lk.Lock()
720+
defer pc.lk.Unlock()
721+
pc.broken = true
722+
}
723+
672724
func (pc *persistConn) close() {
673725
pc.lk.Lock()
674726
defer pc.lk.Unlock()

src/pkg/net/http/transport_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,30 @@ func TestIssue3644(t *testing.T) {
833833
}
834834
}
835835

836+
// Test that a client receives a server's reply, even if the server doesn't read
837+
// the entire request body.
838+
func TestIssue3595(t *testing.T) {
839+
const deniedMsg = "sorry, denied."
840+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
841+
Error(w, deniedMsg, StatusUnauthorized)
842+
}))
843+
defer ts.Close()
844+
tr := &Transport{}
845+
c := &Client{Transport: tr}
846+
res, err := c.Post(ts.URL, "application/octet-stream", neverEnding('a'))
847+
if err != nil {
848+
t.Errorf("Post: %v", err)
849+
return
850+
}
851+
got, err := ioutil.ReadAll(res.Body)
852+
if err != nil {
853+
t.Fatalf("Body ReadAll: %v", err)
854+
}
855+
if !strings.Contains(string(got), deniedMsg) {
856+
t.Errorf("Known bug: response %q does not contain %q", got, deniedMsg)
857+
}
858+
}
859+
836860
type fooProto struct{}
837861

838862
func (fooProto) RoundTrip(req *Request) (*Response, error) {

0 commit comments

Comments
 (0)