Skip to content

Commit d88b137

Browse files
committed
net/http, net/http/httptrace: make Transport support 1xx responses properly
Previously the Transport had good support for 100 Continue responses, but other 1xx informational responses were returned as-is. But per https://tools.ietf.org/html/rfc7231#section-6.2: > A client MUST be able to parse one or more 1xx responses received > prior to a final response, even if the client does not expect one. A > user agent MAY ignore unexpected 1xx responses. We weren't doing that. Instead, we were returning any 1xx that wasn't 100 as the final result. With this change we instead loop over up to 5 (arbitrary) 1xx responses until we find the final one, returning an error if there's more than 5. The limit is just there to guard against malicious servers and to have _some_ limit. By default we ignore the 1xx responses, unless the user defines the new httptrace.ClientTrace.Got1xxResponse hook, which is an expanded version of the previous ClientTrace.Got100Continue. Still remaining: * httputil.ReverseProxy work. (From rfc7231#section-6.2: "A proxy MUST forward 1xx responses unless the proxy itself requested the generation of the 1xx response."). Which would require: * Support for an http.Handler to generate 1xx informational responses. Those can happen later. Fixing the Transport to be resilient to others using 1xx in the future without negotiation (as is being discussed with HTTP status 103) is most important for now. Updates #17739 Change-Id: I55aae8cd978164643fccb9862cd60a230e430486 Reviewed-on: https://go-review.googlesource.com/116855 Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent 29b631e commit d88b137

File tree

4 files changed

+103
-23
lines changed

4 files changed

+103
-23
lines changed

src/go/build/deps_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ var pkgDeps = map[string][]string{
416416
"syscall/js",
417417
},
418418
"net/http/internal": {"L4"},
419-
"net/http/httptrace": {"context", "crypto/tls", "internal/nettrace", "net", "reflect", "time"},
419+
"net/http/httptrace": {"context", "crypto/tls", "internal/nettrace", "net", "net/textproto", "reflect", "time"},
420420

421421
// HTTP-using packages.
422422
"expvar": {"L4", "OS", "encoding/json", "net/http"},

src/net/http/httptrace/trace.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"crypto/tls"
1212
"internal/nettrace"
1313
"net"
14+
"net/textproto"
1415
"reflect"
1516
"time"
1617
)
@@ -107,6 +108,12 @@ type ClientTrace struct {
107108
// Continue" response.
108109
Got100Continue func()
109110

111+
// Got1xxResponse is called for each 1xx informational response header
112+
// returned before the final non-1xx response. Got1xxResponse is called
113+
// for "100 Continue" responses, even if Got100Continue is also defined.
114+
// If it returns an error, the client request is aborted with that error value.
115+
Got1xxResponse func(code int, header textproto.MIMEHeader) error
116+
110117
// DNSStart is called when a DNS lookup begins.
111118
DNSStart func(DNSStartInfo)
112119

src/net/http/transport.go

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"log"
2222
"net"
2323
"net/http/httptrace"
24+
"net/textproto"
2425
"net/url"
2526
"os"
2627
"strings"
@@ -1641,26 +1642,42 @@ func (pc *persistConn) readResponse(rc requestAndChan, trace *httptrace.ClientTr
16411642
trace.GotFirstResponseByte()
16421643
}
16431644
}
1644-
resp, err = ReadResponse(pc.br, rc.req)
1645-
if err != nil {
1646-
return
1647-
}
1648-
if rc.continueCh != nil {
1649-
if resp.StatusCode == 100 {
1650-
if trace != nil && trace.Got100Continue != nil {
1651-
trace.Got100Continue()
1652-
}
1653-
rc.continueCh <- struct{}{}
1654-
} else {
1655-
close(rc.continueCh)
1656-
}
1657-
}
1658-
if resp.StatusCode == 100 {
1659-
pc.readLimit = pc.maxHeaderResponseSize() // reset the limit
1645+
num1xx := 0 // number of informational 1xx headers received
1646+
const max1xxResponses = 5 // arbitrary bound on number of informational responses
1647+
1648+
continueCh := rc.continueCh
1649+
for {
16601650
resp, err = ReadResponse(pc.br, rc.req)
16611651
if err != nil {
16621652
return
16631653
}
1654+
resCode := resp.StatusCode
1655+
if continueCh != nil {
1656+
if resCode == 100 {
1657+
if trace != nil && trace.Got100Continue != nil {
1658+
trace.Got100Continue()
1659+
}
1660+
continueCh <- struct{}{}
1661+
continueCh = nil
1662+
} else if resCode >= 200 {
1663+
close(continueCh)
1664+
continueCh = nil
1665+
}
1666+
}
1667+
if 100 <= resCode && resCode <= 199 {
1668+
num1xx++
1669+
if num1xx > max1xxResponses {
1670+
return nil, errors.New("net/http: too many 1xx informational responses")
1671+
}
1672+
pc.readLimit = pc.maxHeaderResponseSize() // reset the limit
1673+
if trace != nil && trace.Got1xxResponse != nil {
1674+
if err := trace.Got1xxResponse(resCode, textproto.MIMEHeader(resp.Header)); err != nil {
1675+
return nil, err
1676+
}
1677+
}
1678+
continue
1679+
}
1680+
break
16641681
}
16651682
resp.TLS = pc.tlsState
16661683
return

src/net/http/transport_test.go

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"net/http/httptrace"
3232
"net/http/httputil"
3333
"net/http/internal"
34+
"net/textproto"
3435
"net/url"
3536
"os"
3637
"reflect"
@@ -2287,6 +2288,7 @@ Content-Length: %d
22872288
c := &Client{Transport: tr}
22882289

22892290
testResponse := func(req *Request, name string, wantCode int) {
2291+
t.Helper()
22902292
res, err := c.Do(req)
22912293
if err != nil {
22922294
t.Fatalf("%s: Do: %v", name, err)
@@ -2309,13 +2311,67 @@ Content-Length: %d
23092311
req.Header.Set("Request-Id", reqID(i))
23102312
testResponse(req, fmt.Sprintf("100, %d/%d", i, numReqs), 200)
23112313
}
2314+
}
23122315

2313-
// And some other informational 1xx but non-100 responses, to test
2314-
// we return them but don't re-use the connection.
2315-
for i := 1; i <= numReqs; i++ {
2316-
req, _ := NewRequest("POST", "http://other.tld/", strings.NewReader(reqBody(i)))
2317-
req.Header.Set("X-Want-Response-Code", "123 Sesame Street")
2318-
testResponse(req, fmt.Sprintf("123, %d/%d", i, numReqs), 123)
2316+
// Issue 17739: the HTTP client must ignore any unknown 1xx
2317+
// informational responses before the actual response.
2318+
func TestTransportIgnore1xxResponses(t *testing.T) {
2319+
setParallel(t)
2320+
defer afterTest(t)
2321+
cst := newClientServerTest(t, h1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
2322+
conn, buf, _ := w.(Hijacker).Hijack()
2323+
buf.Write([]byte("HTTP/1.1 123 OneTwoThree\r\nFoo: bar\r\n\r\nHTTP/1.1 200 OK\r\nBar: baz\r\nContent-Length: 5\r\n\r\nHello"))
2324+
buf.Flush()
2325+
conn.Close()
2326+
}))
2327+
defer cst.close()
2328+
cst.tr.DisableKeepAlives = true // prevent log spam; our test server is hanging up anyway
2329+
2330+
var got bytes.Buffer
2331+
2332+
req, _ := NewRequest("GET", cst.ts.URL, nil)
2333+
req = req.WithContext(httptrace.WithClientTrace(context.Background(), &httptrace.ClientTrace{
2334+
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
2335+
fmt.Fprintf(&got, "1xx: code=%v, header=%v\n", code, header)
2336+
return nil
2337+
},
2338+
}))
2339+
res, err := cst.c.Do(req)
2340+
if err != nil {
2341+
t.Fatal(err)
2342+
}
2343+
defer res.Body.Close()
2344+
2345+
res.Write(&got)
2346+
want := "1xx: code=123, header=map[Foo:[bar]]\nHTTP/1.1 200 OK\r\nContent-Length: 5\r\nBar: baz\r\n\r\nHello"
2347+
if got.String() != want {
2348+
t.Errorf(" got: %q\nwant: %q\n", got.Bytes(), want)
2349+
}
2350+
}
2351+
2352+
func TestTransportLimits1xxResponses(t *testing.T) {
2353+
setParallel(t)
2354+
defer afterTest(t)
2355+
cst := newClientServerTest(t, h1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
2356+
conn, buf, _ := w.(Hijacker).Hijack()
2357+
for i := 0; i < 10; i++ {
2358+
buf.Write([]byte("HTTP/1.1 123 OneTwoThree\r\n\r\n"))
2359+
}
2360+
buf.Write([]byte("HTTP/1.1 204 No Content\r\n\r\n"))
2361+
buf.Flush()
2362+
conn.Close()
2363+
}))
2364+
defer cst.close()
2365+
cst.tr.DisableKeepAlives = true // prevent log spam; our test server is hanging up anyway
2366+
2367+
res, err := cst.c.Get(cst.ts.URL)
2368+
if res != nil {
2369+
defer res.Body.Close()
2370+
}
2371+
got := fmt.Sprint(err)
2372+
wantSub := "too many 1xx informational responses"
2373+
if !strings.Contains(got, wantSub) {
2374+
t.Errorf("Get error = %v; want substring %q", err, wantSub)
23192375
}
23202376
}
23212377

0 commit comments

Comments
 (0)