Skip to content

Commit d440d19

Browse files
committed
net/http: reverseproxy: forward 1xx responses
Support for 1xx responses has recently been merged in net/http (golang#42597). As discussed in this CL (https://go-review.googlesource.com/c/go/+/269997/comments/1ff70bef_c25a829a), support for forwarding 1xx responses in ReverseProxy has been extracted in this separate patch. According to RFC 7231, "a proxy MUST forward 1xx responses unless the proxy itself requested the generation of the 1xx response". Consequently, all received 1xx responses are automatically forwarded as long as the underlying transport supports ClientTrace.Got1xxResponse. Fixes golang#26088 Fixes golang#51914
1 parent 4381c61 commit d440d19

File tree

2 files changed

+105
-1
lines changed

2 files changed

+105
-1
lines changed

src/net/http/httputil/reverseproxy.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"mime"
1616
"net"
1717
"net/http"
18+
"net/http/httptrace"
1819
"net/http/internal/ascii"
1920
"net/textproto"
2021
"net/url"
@@ -96,6 +97,9 @@ func (r *ProxyRequest) SetXForwarded() {
9697
// ReverseProxy is an HTTP Handler that takes an incoming request and
9798
// sends it to another server, proxying the response back to the
9899
// client.
100+
//
101+
// 1xx responses are forwarded to the client if the underlying
102+
// transport supports ClientTrace.Got1xxResponse.
99103
type ReverseProxy struct {
100104
// Rewrite must be a function which modifies
101105
// the request into a new request to be sent
@@ -449,6 +453,23 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
449453
outreq.Header.Set("User-Agent", "")
450454
}
451455

456+
var headerSet bool
457+
trace := &httptrace.ClientTrace{
458+
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
459+
h := rw.Header()
460+
copyHeader(h, http.Header(header))
461+
rw.WriteHeader(code)
462+
463+
// Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses
464+
for k, _ := range h {
465+
h.Del(k)
466+
}
467+
468+
return nil
469+
},
470+
}
471+
outreq = outreq.WithContext(httptrace.WithClientTrace(outreq.Context(), trace))
472+
452473
res, err := transport.RoundTrip(outreq)
453474
if err != nil {
454475
p.getErrorHandler()(rw, outreq, err)
@@ -470,7 +491,14 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
470491
return
471492
}
472493

473-
copyHeader(rw.Header(), res.Header)
494+
h := rw.Header()
495+
if headerSet {
496+
for k, _ := range h {
497+
h.Del(k)
498+
}
499+
}
500+
501+
copyHeader(h, res.Header)
474502

475503
// The "Trailer" header isn't included in the Transport's response,
476504
// at least for *http.Transport. Build it up from Trailer.

src/net/http/httputil/reverseproxy_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import (
1616
"log"
1717
"net/http"
1818
"net/http/httptest"
19+
"net/http/httptrace"
1920
"net/http/internal/ascii"
21+
"net/textproto"
2022
"net/url"
2123
"os"
2224
"reflect"
@@ -1671,3 +1673,77 @@ func TestReverseProxyRewriteReplacesOut(t *testing.T) {
16711673
t.Errorf("got response %q, want %q", got, want)
16721674
}
16731675
}
1676+
1677+
func Test1xxResponses(t *testing.T) {
1678+
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1679+
h := w.Header()
1680+
h.Add("Link", "</style.css>; rel=preload; as=style")
1681+
h.Add("Link", "</script.js>; rel=preload; as=script")
1682+
w.WriteHeader(http.StatusEarlyHints)
1683+
1684+
h.Add("Link", "</foo.js>; rel=preload; as=script")
1685+
w.WriteHeader(http.StatusProcessing)
1686+
1687+
w.Write([]byte("Hello"))
1688+
}))
1689+
defer backend.Close()
1690+
backendURL, err := url.Parse(backend.URL)
1691+
if err != nil {
1692+
t.Fatal(err)
1693+
}
1694+
proxyHandler := NewSingleHostReverseProxy(backendURL)
1695+
proxyHandler.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
1696+
frontend := httptest.NewServer(proxyHandler)
1697+
defer frontend.Close()
1698+
frontendClient := frontend.Client()
1699+
1700+
checkLinkHeaders := func(t *testing.T, expected, got []string) {
1701+
t.Helper()
1702+
1703+
if len(expected) != len(got) {
1704+
t.Errorf("Expected %d link headers; got %d", len(expected), len(got))
1705+
}
1706+
1707+
for i := range expected {
1708+
if expected[i] != got[i] {
1709+
t.Errorf("Expected %q link header; got %q", expected[i], got[i])
1710+
}
1711+
}
1712+
}
1713+
1714+
var respCounter uint8
1715+
trace := &httptrace.ClientTrace{
1716+
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
1717+
switch code {
1718+
case http.StatusEarlyHints:
1719+
checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script"}, header["Link"])
1720+
case http.StatusProcessing:
1721+
checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script", "</foo.js>; rel=preload; as=script"}, header["Link"])
1722+
default:
1723+
t.Error("Unexpected 1xx response")
1724+
}
1725+
1726+
respCounter++
1727+
1728+
return nil
1729+
},
1730+
}
1731+
req, _ := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), "GET", frontend.URL, nil)
1732+
1733+
res, err := frontendClient.Do(req)
1734+
if err != nil {
1735+
t.Fatalf("Get: %v", err)
1736+
}
1737+
1738+
defer res.Body.Close()
1739+
1740+
if respCounter != 2 {
1741+
t.Errorf("Excpected 2 1xx responses; got %d", respCounter)
1742+
}
1743+
checkLinkHeaders(t, []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script", "</foo.js>; rel=preload; as=script"}, res.Header["Link"])
1744+
1745+
body, _ := io.ReadAll(res.Body)
1746+
if string(body) != "Hello" {
1747+
t.Errorf("Read body %q; want Hello", body)
1748+
}
1749+
}

0 commit comments

Comments
 (0)