Skip to content

Commit dfcd406

Browse files
net/http: disable 100 continue status after handler finished
When client supplies "Expect: 100-continue" header, server wraps request body into expectContinueReader that writes 100 Continue status on the first body read. When handler acts as a reverse proxy and passes incoming request (or body) to the client (or transport) it may happen that request body is read after handler exists which may cause nil pointer panic on connection write if server already closed the connection. This change disables write of 100 Continue status by expectContinueReader after handler finished and before connection is closed. It also fixes racy access to w.wroteContinue. Fixes golang#53808 Updates golang#46866
1 parent bb523c9 commit dfcd406

File tree

3 files changed

+73
-28
lines changed

3 files changed

+73
-28
lines changed

src/net/http/clientserver_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1754,3 +1754,28 @@ func testEarlyHintsRequest(t *testing.T, mode testMode) {
17541754
t.Errorf("Read body %q; want Hello", body)
17551755
}
17561756
}
1757+
1758+
// Issue 53808
1759+
func TestServerReadAfterHandlerDone100Continue(t *testing.T) {
1760+
run(t, testServerReadAfterHandlerDone100Continue)
1761+
}
1762+
func testServerReadAfterHandlerDone100Continue(t *testing.T, mode testMode) {
1763+
readyc := make(chan struct{})
1764+
cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
1765+
go func() {
1766+
<-readyc
1767+
io.ReadAll(r.Body)
1768+
<-readyc
1769+
}()
1770+
}))
1771+
1772+
req, _ := NewRequest("GET", cst.ts.URL, strings.NewReader("body"))
1773+
req.Header.Set("Expect", "100-continue")
1774+
res, err := cst.c.Do(req)
1775+
if err != nil {
1776+
t.Fatalf("Get(%q) = %v", cst.ts.URL, err)
1777+
}
1778+
res.Body.Close()
1779+
readyc <- struct{}{} // server starts reading from the request body
1780+
readyc <- struct{}{} // server finishes reading from the request body
1781+
}

src/net/http/server.go

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,6 @@ type response struct {
425425
reqBody io.ReadCloser
426426
cancelCtx context.CancelFunc // when ServeHTTP exits
427427
wroteHeader bool // a non-1xx header has been (logically) written
428-
wroteContinue bool // 100 Continue response was written
429428
wants10KeepAlive bool // HTTP/1.0 w/ Connection "keep-alive"
430429
wantsClose bool // HTTP request has Connection "close"
431430

@@ -438,6 +437,7 @@ type response struct {
438437
// against the main writer.
439438
canWriteContinue atomic.Bool
440439
writeContinueMu sync.Mutex
440+
wroteContinue bool // 100 Continue response was written
441441

442442
w *bufio.Writer // buffers output in chunks to chunkWriter
443443
cw chunkWriter
@@ -916,17 +916,7 @@ func (ecr *expectContinueReader) Read(p []byte) (n int, err error) {
916916
if ecr.closed.Load() {
917917
return 0, ErrBodyReadAfterClose
918918
}
919-
w := ecr.resp
920-
if !w.wroteContinue && w.canWriteContinue.Load() && !w.conn.hijacked() {
921-
w.wroteContinue = true
922-
w.writeContinueMu.Lock()
923-
if w.canWriteContinue.Load() {
924-
w.conn.bufw.WriteString("HTTP/1.1 100 Continue\r\n\r\n")
925-
w.conn.bufw.Flush()
926-
w.canWriteContinue.Store(false)
927-
}
928-
w.writeContinueMu.Unlock()
929-
}
919+
ecr.resp.writeContinueOnce()
930920
n, err = ecr.readCloser.Read(p)
931921
if err == io.EOF {
932922
ecr.sawEOF.Store(true)
@@ -1165,10 +1155,8 @@ func (w *response) WriteHeader(code int) {
11651155
// so it takes the non-informational path.
11661156
if code >= 100 && code <= 199 && code != StatusSwitchingProtocols {
11671157
// Prevent a potential race with an automatically-sent 100 Continue triggered by Request.Body.Read()
1168-
if code == 100 && w.canWriteContinue.Load() {
1169-
w.writeContinueMu.Lock()
1170-
w.canWriteContinue.Store(false)
1171-
w.writeContinueMu.Unlock()
1158+
if code == 100 {
1159+
w.disableContinue()
11721160
}
11731161

11741162
writeStatusLine(w.conn.bufw, w.req.ProtoAtLeast(1, 1), code, w.statusBuf[:])
@@ -1383,9 +1371,11 @@ func (cw *chunkWriter) writeHeader(p []byte) {
13831371

13841372
switch bdy := w.req.Body.(type) {
13851373
case *expectContinueReader:
1374+
bdy.resp.writeContinueMu.Lock()
13861375
if bdy.resp.wroteContinue {
13871376
discard = true
13881377
}
1378+
bdy.resp.writeContinueMu.Unlock()
13891379
case *body:
13901380
bdy.mu.Lock()
13911381
switch {
@@ -1625,15 +1615,7 @@ func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err er
16251615
return 0, ErrHijacked
16261616
}
16271617

1628-
if w.canWriteContinue.Load() {
1629-
// Body reader wants to write 100 Continue but hasn't yet.
1630-
// Tell it not to. The store must be done while holding the lock
1631-
// because the lock makes sure that there is not an active write
1632-
// this very moment.
1633-
w.writeContinueMu.Lock()
1634-
w.canWriteContinue.Store(false)
1635-
w.writeContinueMu.Unlock()
1636-
}
1618+
w.disableContinue()
16371619

16381620
if !w.wroteHeader {
16391621
w.WriteHeader(StatusOK)
@@ -1679,6 +1661,29 @@ func (w *response) finishRequest() {
16791661
}
16801662
}
16811663

1664+
// disableContinue disables write of 100 Continue status.
1665+
func (w *response) disableContinue() {
1666+
if w.canWriteContinue.Load() {
1667+
w.writeContinueMu.Lock()
1668+
w.canWriteContinue.Store(false)
1669+
w.writeContinueMu.Unlock()
1670+
}
1671+
}
1672+
1673+
// writeContinueOnce writes 100 Continue status if allowed.
1674+
func (w *response) writeContinueOnce() {
1675+
if w.canWriteContinue.Load() && !w.conn.hijacked() {
1676+
w.writeContinueMu.Lock()
1677+
if w.canWriteContinue.Load() {
1678+
w.conn.bufw.WriteString("HTTP/1.1 100 Continue\r\n\r\n")
1679+
w.conn.bufw.Flush()
1680+
w.canWriteContinue.Store(false)
1681+
w.wroteContinue = true
1682+
}
1683+
w.writeContinueMu.Unlock()
1684+
}
1685+
}
1686+
16821687
// shouldReuseConnection reports whether the underlying TCP connection can be reused.
16831688
// It must only be called after the handler is done executing.
16841689
func (w *response) shouldReuseConnection() bool {
@@ -1905,6 +1910,7 @@ func (c *conn) serve(ctx context.Context) {
19051910
if inFlightResponse != nil {
19061911
inFlightResponse.conn.r.abortPendingRead()
19071912
inFlightResponse.reqBody.Close()
1913+
inFlightResponse.disableContinue()
19081914
}
19091915
c.close()
19101916
c.setState(c.rwc, StateClosed, runHooks)
@@ -2046,6 +2052,7 @@ func (c *conn) serve(ctx context.Context) {
20462052
return
20472053
}
20482054
w.finishRequest()
2055+
w.disableContinue()
20492056
c.rwc.SetWriteDeadline(time.Time{})
20502057
if !w.shouldReuseConnection() {
20512058
if w.requestBodyLimitHit || w.closedRequestBodyEarly() {

src/net/http/transport_test.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"log"
2727
mrand "math/rand"
2828
"net"
29+
"net/http"
2930
. "net/http"
3031
"net/http/httptest"
3132
"net/http/httptrace"
@@ -6901,19 +6902,31 @@ func testHandlerAbortRacesBodyRead(t *testing.T, mode testMode) {
69016902
panic(ErrAbortHandler)
69026903
})).ts
69036904

6905+
newRequest := func() *http.Request {
6906+
const reqLen = 6 * 1024 * 1024
6907+
req, _ := NewRequest("POST", ts.URL, &io.LimitedReader{R: neverEnding('x'), N: reqLen})
6908+
req.ContentLength = reqLen
6909+
return req
6910+
}
6911+
69046912
var wg sync.WaitGroup
69056913
for i := 0; i < 2; i++ {
69066914
wg.Add(1)
69076915
go func() {
69086916
defer wg.Done()
69096917
for j := 0; j < 10; j++ {
6910-
const reqLen = 6 * 1024 * 1024
6911-
req, _ := NewRequest("POST", ts.URL, &io.LimitedReader{R: neverEnding('x'), N: reqLen})
6912-
req.ContentLength = reqLen
6918+
req := newRequest()
69136919
resp, _ := ts.Client().Transport.RoundTrip(req)
69146920
if resp != nil {
69156921
resp.Body.Close()
69166922
}
6923+
6924+
req = newRequest()
6925+
req.Header.Set("Expect", "100-continue")
6926+
resp, _ = ts.Client().Transport.RoundTrip(req)
6927+
if resp != nil {
6928+
resp.Body.Close()
6929+
}
69176930
}
69186931
}()
69196932
}

0 commit comments

Comments
 (0)