Skip to content

Commit cb0d998

Browse files
committed
net/http: do not discard body content when closing it within request handlers
(*body).Close() internally tries to discard the content of a request body up to 256 KB. We rely on this behavior to allow connection re-use, by calling (*body).Close() when our request handler exits. Unfortunately, this causes an unfortunate side-effect where we would prematurely try to discard a body content when (*body).Close() is called from within a request handler. There should not be a good reason for (*body).Close() to do this when called from within a request handler. As such, this CL modifies (*body).Close() to not discard body contents when called from within a request handler. Note that when a request handler exits, it will still try to discard the body content for connection re-use. For #75933 Change-Id: I71d2431a540579184066dd35d3da49d6c85c3daf Reviewed-on: https://go-review.googlesource.com/c/go/+/720380 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Nicholas Husin <[email protected]> Reviewed-by: Damien Neil <[email protected]>
1 parent 03ed439 commit cb0d998

File tree

3 files changed

+185
-143
lines changed

3 files changed

+185
-143
lines changed

src/net/http/serve_test.go

Lines changed: 102 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2150,118 +2150,137 @@ func TestServerUnreadRequestBodyLarge(t *testing.T) {
21502150
}
21512151
}
21522152

2153-
type handlerBodyCloseTest struct {
2153+
type bodyDiscardTest struct {
21542154
bodySize int
21552155
bodyChunked bool
21562156
reqConnClose bool
21572157

2158-
wantEOFSearch bool // should Handler's Body.Close do Reads, looking for EOF?
2159-
wantNextReq bool // should it find the next request on the same conn?
2158+
shouldDiscardBody bool // should the handler discard body after it exits?
21602159
}
21612160

2162-
func (t handlerBodyCloseTest) connectionHeader() string {
2161+
func (t bodyDiscardTest) connectionHeader() string {
21632162
if t.reqConnClose {
21642163
return "Connection: close\r\n"
21652164
}
21662165
return ""
21672166
}
21682167

2169-
var handlerBodyCloseTests = [...]handlerBodyCloseTest{
2170-
// Small enough to slurp past to the next request +
2171-
// has Content-Length.
2168+
var bodyDiscardTests = [...]bodyDiscardTest{
2169+
// Have:
2170+
// - Small body.
2171+
// - Content-Length defined.
2172+
// Should:
2173+
// - Discard remaining body.
21722174
0: {
2173-
bodySize: 20 << 10,
2174-
bodyChunked: false,
2175-
reqConnClose: false,
2176-
wantEOFSearch: true,
2177-
wantNextReq: true,
2175+
bodySize: 20 << 10,
2176+
bodyChunked: false,
2177+
reqConnClose: false,
2178+
shouldDiscardBody: true,
21782179
},
21792180

2180-
// Small enough to slurp past to the next request +
2181-
// is chunked.
2181+
// Have:
2182+
// - Small body.
2183+
// - Chunked (no Content-Length defined).
2184+
// Should:
2185+
// - Discard remaining body.
21822186
1: {
2183-
bodySize: 20 << 10,
2184-
bodyChunked: true,
2185-
reqConnClose: false,
2186-
wantEOFSearch: true,
2187-
wantNextReq: true,
2187+
bodySize: 20 << 10,
2188+
bodyChunked: true,
2189+
reqConnClose: false,
2190+
shouldDiscardBody: true,
21882191
},
21892192

2190-
// Small enough to slurp past to the next request +
2191-
// has Content-Length +
2192-
// declares Connection: close (so pointless to read more).
2193+
// Have:
2194+
// - Small body.
2195+
// - Content-Length defined.
2196+
// - Connection: close.
2197+
// Should:
2198+
// - Not discard remaining body (no point as Connection: close).
21932199
2: {
2194-
bodySize: 20 << 10,
2195-
bodyChunked: false,
2196-
reqConnClose: true,
2197-
wantEOFSearch: false,
2198-
wantNextReq: false,
2200+
bodySize: 20 << 10,
2201+
bodyChunked: false,
2202+
reqConnClose: true,
2203+
shouldDiscardBody: false,
21992204
},
22002205

2201-
// Small enough to slurp past to the next request +
2202-
// declares Connection: close,
2203-
// but chunked, so it might have trailers.
2204-
// TODO: maybe skip this search if no trailers were declared
2205-
// in the headers.
2206+
// Have:
2207+
// - Small body.
2208+
// - Chunked (no Content-Length defined).
2209+
// - Connection: close.
2210+
// Should:
2211+
// - Discard remaining body (chunked, so it might have trailers).
2212+
//
2213+
// TODO: maybe skip this if no trailers were declared in the headers.
22062214
3: {
2207-
bodySize: 20 << 10,
2208-
bodyChunked: true,
2209-
reqConnClose: true,
2210-
wantEOFSearch: true,
2211-
wantNextReq: false,
2215+
bodySize: 20 << 10,
2216+
bodyChunked: true,
2217+
reqConnClose: true,
2218+
shouldDiscardBody: true,
22122219
},
22132220

2214-
// Big with Content-Length, so give up immediately if we know it's too big.
2221+
// Have:
2222+
// - Large body.
2223+
// - Content-Length defined.
2224+
// Should:
2225+
// - Not discard remaining body (we know it is too large from Content-Length).
22152226
4: {
2216-
bodySize: 1 << 20,
2217-
bodyChunked: false, // has a Content-Length
2218-
reqConnClose: false,
2219-
wantEOFSearch: false,
2220-
wantNextReq: false,
2227+
bodySize: 1 << 20,
2228+
bodyChunked: false,
2229+
reqConnClose: false,
2230+
shouldDiscardBody: false,
22212231
},
22222232

2223-
// Big chunked, so read a bit before giving up.
2233+
// Have:
2234+
// - Large body.
2235+
// - Chunked (no Content-Length defined).
2236+
// Should:
2237+
// - Discard remaining body (chunked, so we try up to a limit before giving up).
22242238
5: {
2225-
bodySize: 1 << 20,
2226-
bodyChunked: true,
2227-
reqConnClose: false,
2228-
wantEOFSearch: true,
2229-
wantNextReq: false,
2239+
bodySize: 1 << 20,
2240+
bodyChunked: true,
2241+
reqConnClose: false,
2242+
shouldDiscardBody: true,
22302243
},
22312244

2232-
// Big with Connection: close, but chunked, so search for trailers.
2233-
// TODO: maybe skip this search if no trailers were declared
2234-
// in the headers.
2245+
// Have:
2246+
// - Large body.
2247+
// - Content-Length defined.
2248+
// - Connection: close.
2249+
// Should:
2250+
// - Not discard remaining body (Connection: Close, and Content-Length is too large).
22352251
6: {
2236-
bodySize: 1 << 20,
2237-
bodyChunked: true,
2238-
reqConnClose: true,
2239-
wantEOFSearch: true,
2240-
wantNextReq: false,
2252+
bodySize: 1 << 20,
2253+
bodyChunked: false,
2254+
reqConnClose: true,
2255+
shouldDiscardBody: false,
22412256
},
2242-
2243-
// Big with Connection: close, so don't do any reads on Close.
2244-
// With Content-Length.
2257+
// Have:
2258+
// - Large body.
2259+
// - Chunked (no Content-Length defined).
2260+
// - Connection: close.
2261+
// Should:
2262+
// - Discard remaining body (chunked, so it might have trailers).
2263+
//
2264+
// TODO: maybe skip this if no trailers were declared in the headers.
22452265
7: {
2246-
bodySize: 1 << 20,
2247-
bodyChunked: false,
2248-
reqConnClose: true,
2249-
wantEOFSearch: false,
2250-
wantNextReq: false,
2266+
bodySize: 1 << 20,
2267+
bodyChunked: true,
2268+
reqConnClose: true,
2269+
shouldDiscardBody: true,
22512270
},
22522271
}
22532272

2254-
func TestHandlerBodyClose(t *testing.T) {
2273+
func TestBodyDiscard(t *testing.T) {
22552274
setParallel(t)
22562275
if testing.Short() && testenv.Builder() == "" {
22572276
t.Skip("skipping in -short mode")
22582277
}
2259-
for i, tt := range handlerBodyCloseTests {
2260-
testHandlerBodyClose(t, i, tt)
2278+
for i, tt := range bodyDiscardTests {
2279+
testBodyDiscard(t, i, tt)
22612280
}
22622281
}
22632282

2264-
func testHandlerBodyClose(t *testing.T, i int, tt handlerBodyCloseTest) {
2283+
func testBodyDiscard(t *testing.T, i int, tt bodyDiscardTest) {
22652284
conn := new(testConn)
22662285
body := strings.Repeat("x", tt.bodySize)
22672286
if tt.bodyChunked {
@@ -2275,12 +2294,12 @@ func testHandlerBodyClose(t *testing.T, i int, tt handlerBodyCloseTest) {
22752294
cw.Close()
22762295
conn.readBuf.WriteString("\r\n")
22772296
} else {
2278-
conn.readBuf.Write([]byte(fmt.Sprintf(
2297+
conn.readBuf.Write(fmt.Appendf(nil,
22792298
"POST / HTTP/1.1\r\n"+
22802299
"Host: test\r\n"+
22812300
tt.connectionHeader()+
22822301
"Content-Length: %d\r\n"+
2283-
"\r\n", len(body))))
2302+
"\r\n", len(body)))
22842303
conn.readBuf.Write([]byte(body))
22852304
}
22862305
if !tt.reqConnClose {
@@ -2295,26 +2314,23 @@ func testHandlerBodyClose(t *testing.T, i int, tt handlerBodyCloseTest) {
22952314
}
22962315

22972316
ls := &oneConnListener{conn}
2298-
var numReqs int
2299-
var size0, size1 int
2317+
var initialSize, closedSize, exitSize int
23002318
go Serve(ls, HandlerFunc(func(rw ResponseWriter, req *Request) {
2301-
numReqs++
2302-
if numReqs == 1 {
2303-
size0 = readBufLen()
2304-
req.Body.Close()
2305-
size1 = readBufLen()
2306-
}
2319+
initialSize = readBufLen()
2320+
req.Body.Close()
2321+
closedSize = readBufLen()
23072322
}))
23082323
<-conn.closec
2309-
if numReqs < 1 || numReqs > 2 {
2310-
t.Fatalf("%d. bug in test. unexpected number of requests = %d", i, numReqs)
2324+
exitSize = readBufLen()
2325+
2326+
if initialSize != closedSize {
2327+
t.Errorf("%d. Close() within request handler should be a no-op, but body size went from %d to %d", i, initialSize, closedSize)
23112328
}
2312-
didSearch := size0 != size1
2313-
if didSearch != tt.wantEOFSearch {
2314-
t.Errorf("%d. did EOF search = %v; want %v (size went from %d to %d)", i, didSearch, !didSearch, size0, size1)
2329+
if tt.shouldDiscardBody && closedSize <= exitSize {
2330+
t.Errorf("%d. want body content to be discarded upon request handler exit, but size went from %d to %d", i, closedSize, exitSize)
23152331
}
2316-
if tt.wantNextReq && numReqs != 2 {
2317-
t.Errorf("%d. numReq = %d; want 2", i, numReqs)
2332+
if !tt.shouldDiscardBody && closedSize != exitSize {
2333+
t.Errorf("%d. want body content to not be discarded upon request handler exit, but size went from %d to %d", i, closedSize, exitSize)
23182334
}
23192335
}
23202336

src/net/http/server.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,9 +1077,6 @@ func (c *conn) readRequest(ctx context.Context) (w *response, err error) {
10771077
req.ctx = ctx
10781078
req.RemoteAddr = c.remoteAddr
10791079
req.TLS = c.tlsState
1080-
if body, ok := req.Body.(*body); ok {
1081-
body.doEarlyClose = true
1082-
}
10831080

10841081
// Adjust the read deadline if necessary.
10851082
if !hdrDeadline.Equal(wholeReqDeadline) {
@@ -1711,9 +1708,11 @@ func (w *response) finishRequest() {
17111708

17121709
w.conn.r.abortPendingRead()
17131710

1714-
// Close the body (regardless of w.closeAfterReply) so we can
1715-
// re-use its bufio.Reader later safely.
1716-
w.reqBody.Close()
1711+
// Try to discard the body (regardless of w.closeAfterReply), so we can
1712+
// potentially reuse it in the same connection.
1713+
if b, ok := w.reqBody.(*body); ok {
1714+
b.tryDiscardBody()
1715+
}
17171716

17181717
if w.req.MultipartForm != nil {
17191718
w.req.MultipartForm.RemoveAll()
@@ -1741,16 +1740,16 @@ func (w *response) shouldReuseConnection() bool {
17411740
return false
17421741
}
17431742

1744-
if w.closedRequestBodyEarly() {
1743+
if w.didIncompleteDiscard() {
17451744
return false
17461745
}
17471746

17481747
return true
17491748
}
17501749

1751-
func (w *response) closedRequestBodyEarly() bool {
1750+
func (w *response) didIncompleteDiscard() bool {
17521751
body, ok := w.req.Body.(*body)
1753-
return ok && body.didEarlyClose()
1752+
return ok && body.didIncompleteDiscard()
17541753
}
17551754

17561755
func (w *response) Flush() {
@@ -2106,6 +2105,18 @@ func (c *conn) serve(ctx context.Context) {
21062105
// But we're not going to implement HTTP pipelining because it
21072106
// was never deployed in the wild and the answer is HTTP/2.
21082107
inFlightResponse = w
2108+
// Ensure that Close() invocations within request handlers do not
2109+
// discard the body.
2110+
if b, ok := w.reqBody.(*body); ok {
2111+
b.mu.Lock()
2112+
b.inRequestHandler = true
2113+
b.mu.Unlock()
2114+
defer func() {
2115+
b.mu.Lock()
2116+
b.inRequestHandler = false
2117+
b.mu.Unlock()
2118+
}()
2119+
}
21092120
serverHandler{c.server}.ServeHTTP(w, w.req)
21102121
inFlightResponse = nil
21112122
w.cancelCtx()
@@ -2116,7 +2127,7 @@ func (c *conn) serve(ctx context.Context) {
21162127
w.finishRequest()
21172128
c.rwc.SetWriteDeadline(time.Time{})
21182129
if !w.shouldReuseConnection() {
2119-
if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
2130+
if w.requestBodyLimitHit || w.didIncompleteDiscard() {
21202131
c.closeWriteAndWait()
21212132
}
21222133
return

0 commit comments

Comments
 (0)