net/http: prevent server from draining request body if status 100 was not sent When a client sends a request with "Expect: 100-continue", it waits for the server to reply with a status 100 response before transmitting the request body. In some cases, a server might reject the request early, or otherwise do not read the request body in order to generate its responses. In such scenarios, we need to make sure that the server will not try to drain the request body. This ensures that the server will not hang, waiting for a request body that will never arrive. Fixes #75933 Change-Id: Ice63b2fadfc2a72b825fb97975a2bd116a6a6964 Reviewed-on: https://go-review.googlesource.com/c/go/+/793160 LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Damien Neil <dneil@google.com> Reviewed-by: Nicholas Husin <husin@google.com>
diff --git a/src/net/http/serve_test.go b/src/net/http/serve_test.go index f8710af..1205e8b 100644 --- a/src/net/http/serve_test.go +++ b/src/net/http/serve_test.go
@@ -7555,6 +7555,36 @@ readyc <- struct{}{} // server finishes reading from the request body } +// Issue 75933. +func TestServerExpect100ContinueUnreadBody(t *testing.T) { + run(t, testServerExpect100ContinueUnreadBody) +} +func testServerExpect100ContinueUnreadBody(t *testing.T, mode testMode) { + cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { + w.WriteHeader(StatusOK) + // Make sure that Read after not sending status 100 does not hang. + // TODO: Read in this situation should return an error. + io.ReadAll(r.Body) + })) + + req, _ := NewRequest("POST", cst.ts.URL, strings.NewReader("some body")) + req.Header.Set("Expect", "100-continue") + + // Set a short timeout on the client to catch the hang quickly. + cst.c.Timeout = 2 * time.Second + cst.tr.ExpectContinueTimeout = 10 * time.Second + + resp, err := cst.c.Do(req) + if err != nil { + t.Fatalf("Request failed: %v (likely due to hang)", err) + } + defer resp.Body.Close() + + if resp.StatusCode != StatusOK { + t.Errorf("expected 200 OK, got %v", resp.Status) + } +} + func TestInvalidChunkedBodies(t *testing.T) { for _, test := range []struct { name string
diff --git a/src/net/http/server.go b/src/net/http/server.go index 1ef5621..058c42d 100644 --- a/src/net/http/server.go +++ b/src/net/http/server.go
@@ -570,11 +570,30 @@ } } -// disableWriteContinue stops Request.Body.Read from sending an automatic 100-Continue. -// If a 100-Continue is being written, it waits for it to complete before continuing. -func (w *response) disableWriteContinue() { +// disableWriteContinue stops Request.Body.Read from sending an automatic +// 100 Continue. As the name implies, it is only useful when the request +// expects a 100 Continue and the body is wrapped in an expectContinueReader; +// otherwise, it is a no-op. +// If a 100-Continue is being written, it waits for it to complete before +// continuing. If skipDrain is true, it also prevents the server from draining +// the request body and flags the connection to be closed after the reply, as +// the client will never send the body. +func (w *response) disableWriteContinue(skipDrain bool) { + ecr, ok := w.reqBody.(*expectContinueReader) + if !ok { + return + } w.writeContinueMu.Lock() - w.canWriteContinue.Store(false) + if w.canWriteContinue.Load() { + w.canWriteContinue.Store(false) + if skipDrain { + // Make sure that the connection will not be reused by sending + // "Connection: close" header in the response. + w.closeAfterReply = true + // Ensure that the body will not be drained in Close. + ecr.closed.Store(true) + } + } w.writeContinueMu.Unlock() } @@ -983,7 +1002,12 @@ } func (ecr *expectContinueReader) Close() error { - ecr.closed.Store(true) + if ecr.resp.canWriteContinue.Load() { + ecr.resp.disableWriteContinue(true) + } + if ecr.closed.Swap(true) { + return nil + } return ecr.readCloser.Close() } @@ -1185,10 +1209,12 @@ } checkWriteHeaderCode(code) - if code < 101 || code > 199 { - // Sending a 100 Continue or any non-1xx header disables the - // automatically-sent 100 Continue from Request.Body.Read. - w.disableWriteContinue() + // Sending a 100 Continue or any non-1XX header disables the + // automatically-sent 100 Continue from Request.Body.Read. If it is a final + // response (200 or higher), we skip draining the request body, which the + // client will never send. + if code == 100 || code >= 200 { + w.disableWriteContinue(code >= 200) } // Handle informational headers. @@ -1663,7 +1689,7 @@ if w.canWriteContinue.Load() { // Body reader wants to write 100 Continue but hasn't yet. Tell it not to. - w.disableWriteContinue() + w.disableWriteContinue(true) } if !w.wroteHeader { @@ -1701,6 +1727,10 @@ w.conn.r.abortPendingRead() + if w.canWriteContinue.Load() { + w.disableWriteContinue(true) + } + // Close the body (regardless of w.closeAfterReply) so we can // re-use its bufio.Reader later safely. w.reqBody.Close() @@ -1931,7 +1961,7 @@ } if inFlightResponse != nil { inFlightResponse.cancelCtx() - inFlightResponse.disableWriteContinue() + inFlightResponse.disableWriteContinue(true) } if !c.hijacked() { if inFlightResponse != nil { @@ -2094,6 +2124,7 @@ // Wrap the Body reader with one that replies on the connection req.Body = &expectContinueReader{readCloser: req.Body, resp: w} w.canWriteContinue.Store(true) + w.reqBody = req.Body } } else if req.Header.get("Expect") != "" { w.sendExpectationFailed() @@ -2257,7 +2288,7 @@ if w.handlerDone.Load() { panic("net/http: Hijack called after ServeHTTP finished") } - w.disableWriteContinue() + w.disableWriteContinue(false) if w.wroteHeader { w.cw.flush() }