From 107610d561ffd80cd71b4ab7b263cad45c7cd0da Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Tue, 1 Sep 2026 09:56:53 -0400 Subject: [PATCH] Tell the client the connection is closing after each response Signed-off-by: Arpit Jain --- proxy/proxy.go | 7 +++++++ proxy/proxy_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/proxy/proxy.go b/proxy/proxy.go index d3ec4c6..cccf0f4 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -440,6 +440,12 @@ func (p *Server) forwardRequest(conn net.Conn, req *http.Request, https bool, se resp.ProtoMajor = 1 resp.ProtoMinor = 1 + // We serve one request per connection and close it in the caller's defer, + // so the client has to be told, or a pooled socket is reused after we are + // gone (RFC 9112 section 9.6). + resp.Header.Del("Connection") + resp.Close = true + // Copy response back to client err = resp.Write(conn) if err != nil { @@ -492,6 +498,7 @@ For more help: https://github.com/coder/boundary resp.Body = io.NopCloser(strings.NewReader(body)) resp.ContentLength = int64(len(body)) + resp.Close = true // Copy response back to client err := resp.Write(conn) diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index ffd1c8d..f8c5153 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -1,7 +1,11 @@ package proxy import ( + "net/http" + "net/http/httptest" "testing" + + "github.com/stretchr/testify/require" ) // TestProxyServerBasicHTTP tests basic HTTP request handling @@ -63,3 +67,44 @@ func TestProxyServerBasicHTTPS(t *testing.T) { pt.ExpectDeny("https://localhost:8080/", "example.com") }) } + +// The proxy serves one request per connection and then closes it, so both the +// forwarded and the blocked response have to say so. Without the signal a +// client that pools sockets reuses one the proxy has already closed. +func TestProxyResponsesSignalConnectionClose(t *testing.T) { + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + })) + defer backend.Close() + + pt := NewProxyTest(t, WithAllowedDomain("127.0.0.1")).Start() + defer pt.Stop() + + t.Run("allowed request", func(t *testing.T) { + resp, err := pt.proxyClient.Get(backend.URL) + require.NoError(t, err) + defer resp.Body.Close() //nolint:errcheck + + require.Equal(t, http.StatusOK, resp.StatusCode) + require.True(t, resp.Close, "response must tell the client the connection is closing") + }) + + t.Run("blocked request", func(t *testing.T) { + blocked := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer blocked.Close() + + req, err := http.NewRequest(http.MethodGet, blocked.URL, nil) + require.NoError(t, err) + req.Host = "example.com" + + resp, err := pt.proxyClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() //nolint:errcheck + + require.Equal(t, http.StatusForbidden, resp.StatusCode) + require.True(t, resp.Close, "blocked response must tell the client the connection is closing") + }) +}