Skip to content

Commit

Permalink
http2: fix protocol violation regression when writing certain request…
Browse files Browse the repository at this point in the history
… bodies

The mod_h2 workaround CL (git rev 28d1bd4,
https://golang.org/cl/25362) introduced a regression where the
Transport could write two DATA frames, both with END_STREAM, if the
Request.Body returned (non-0, io.EOF). strings.Reader, bytes.Reader
are the most common Reader types used with HTTP requests and they only
return (0, io.EOF) so we got generally lucky and things seemed to
work, but other Readers do return (non-0, io.EOF), like the HTTP
transport/server Readers. This is why we broke the HTTP proxy code,
when proxying to HTTP/2.

Updates golang/go#16788 (fixes after it's bundled into std)

Change-Id: I42684017039eacfc27ee53e9c11431f713fdaca4
Reviewed-on: https://go-review.googlesource.com/27406
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Chris Broadfoot <cbro@golang.org>
  • Loading branch information
bradfitz committed Aug 19, 2016
1 parent 07b5174 commit 7394c11
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 9 deletions.
29 changes: 20 additions & 9 deletions http2/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,15 +622,18 @@ func bodyAndLength(req *http.Request) (body io.Reader, contentLen int64) {
// We have a body but a zero content length. Test to see if
// it's actually zero or just unset.
var buf [1]byte
n, rerr := io.ReadFull(body, buf[:])
n, rerr := body.Read(buf[:])
if rerr != nil && rerr != io.EOF {
return errorReader{rerr}, -1
}
if n == 1 {
// Oh, guess there is data in this Body Reader after all.
// The ContentLength field just wasn't set.
// Stich the Body back together again, re-attaching our
// Stitch the Body back together again, re-attaching our
// consumed byte.
if rerr == io.EOF {
return bytes.NewReader(buf[:]), 1
}
return io.MultiReader(bytes.NewReader(buf[:]), body), -1
}
// Body is actually zero bytes.
Expand Down Expand Up @@ -901,10 +904,11 @@ func (cs *clientStream) writeRequestBody(body io.Reader, bodyCloser io.Closer) (
err = cc.fr.WriteData(cs.ID, sentEnd, data)
if err == nil {
// TODO(bradfitz): this flush is for latency, not bandwidth.
// Most requests won't need this. Make this opt-in or opt-out?
// Use some heuristic on the body type? Nagel-like timers?
// Based on 'n'? Only last chunk of this for loop, unless flow control
// tokens are low? For now, always:
// Most requests won't need this. Make this opt-in or
// opt-out? Use some heuristic on the body type? Nagel-like
// timers? Based on 'n'? Only last chunk of this for loop,
// unless flow control tokens are low? For now, always.
// If we change this, see comment below.
err = cc.bw.Flush()
}
cc.wmu.Unlock()
Expand All @@ -914,8 +918,15 @@ func (cs *clientStream) writeRequestBody(body io.Reader, bodyCloser io.Closer) (
}
}

if sentEnd {
// Already sent END_STREAM (which implies we have no
// trailers) and flushed, because currently all
// WriteData frames above get a flush. So we're done.
return nil
}

var trls []byte
if !sentEnd && hasTrailers {
if hasTrailers {
cc.mu.Lock()
defer cc.mu.Unlock()
trls = cc.encodeTrailers(req)
Expand All @@ -924,8 +935,8 @@ func (cs *clientStream) writeRequestBody(body io.Reader, bodyCloser io.Closer) (
cc.wmu.Lock()
defer cc.wmu.Unlock()

// Avoid forgetting to send an END_STREAM if the encoded
// trailers are 0 bytes. Both results produce and END_STREAM.
// Two ways to send END_STREAM: either with trailers, or
// with an empty DATA frame.
if len(trls) > 0 {
err = cc.writeHeaders(cs.ID, true, trls)
} else {
Expand Down
41 changes: 41 additions & 0 deletions http2/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2427,3 +2427,44 @@ func TestTransportReturnsErrorOnBadResponseHeaders(t *testing.T) {
}
ct.run()
}

// byteAndEOFReader returns is in an io.Reader which reads one byte
// (the underlying byte) and io.EOF at once in its Read call.
type byteAndEOFReader byte

func (b byteAndEOFReader) Read(p []byte) (n int, err error) {
if len(p) == 0 {
panic("unexpected useless call")
}
p[0] = byte(b)
return 1, io.EOF
}

// Issue 16788: the Transport had a regression where it started
// sending a spurious DATA frame with a duplicate END_STREAM bit after
// the request body writer goroutine had already read an EOF from the
// Request.Body and included the END_STREAM on a data-carrying DATA
// frame.
//
// Notably, to trigger this, the requests need to use a Request.Body
// which returns (non-0, io.EOF) and also needs to set the ContentLength
// explicitly.
func TestTransportBodyDoubleEndStream(t *testing.T) {
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
// Nothing.
}, optOnlyServer)
defer st.Close()

tr := &Transport{TLSClientConfig: tlsConfigInsecure}
defer tr.CloseIdleConnections()

for i := 0; i < 2; i++ {
req, _ := http.NewRequest("POST", st.ts.URL, byteAndEOFReader('a'))
req.ContentLength = 1
res, err := tr.RoundTrip(req)
if err != nil {
t.Fatalf("failure on req %d: %v", i+1, err)
}
defer res.Body.Close()
}
}

0 comments on commit 7394c11

Please sign in to comment.