Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions caddytest/integration/reverseproxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package integration

import (
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"runtime"
"strings"
Expand Down Expand Up @@ -893,3 +895,111 @@ func TestWeightedRoundRobinSelectionValidation(t *testing.T) {
})
}
}

func TestReverseProxyResponseHandling(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/header", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Length", "500")
writer.WriteHeader(500)
_ = http.NewResponseController(writer).Flush()
panic(http.ErrAbortHandler)
})
mux.HandleFunc("/partial", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Length", "500")
writer.WriteHeader(500)
_, _ = io.WriteString(writer, "partial")
_ = http.NewResponseController(writer).Flush()
panic(http.ErrAbortHandler)
})
mux.HandleFunc("/full", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Length", "500")
writer.WriteHeader(500)
for range 125 {
_, _ = io.WriteString(writer, "full")
}
_ = http.NewResponseController(writer).Flush()
})
mux.HandleFunc("/empty", func(writer http.ResponseWriter, request *http.Request) {
panic(http.ErrAbortHandler)
})
ts := httptest.NewUnstartedServer(mux)
ts.Start()
t.Cleanup(func() {
ts.Close()
})

tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy %s
}
`, ts.URL), "caddyfile")

for _, tc := range []struct {
endpoint string
status int
bodyType int // 0 = no body, 1 = partial, 2 = full
body string
}{
{
endpoint: "/header",
status: 500,
bodyType: 0,
body: "",
},
{
endpoint: "/partial",
status: 500,
bodyType: 1,
body: "partial",
},
{
endpoint: "/full",
status: 500,
bodyType: 2,
body: strings.Repeat("full", 125),
},
{
endpoint: "/empty",
status: 502,
bodyType: 2,
body: "",
},
Comment on lines +957 to +974

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I reading this correct, partial data is sent to client albeit with code 500? Also, how come empty response gets 502 while partial content gets 500? Is this stdlib doing it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

500 is the status from the backend, caddy is just forwarding the response as is unless configured otherwise.

502 is the response by default if backend doesn't reply

https://github.com/golang/go/blob/1952e618b834bda60fb9efff0fc0af46b38e110d/src/net/http/httputil/reverseproxy.go#L556

https://github.com/golang/go/blob/1952e618b834bda60fb9efff0fc0af46b38e110d/src/net/http/httputil/reverseproxy.go#L376

} {
req, err := http.NewRequest("GET", "http://localhost:9080"+tc.endpoint, nil)
if err != nil {
t.Fatalf("unable to create request %s for endpoint %s", err, tc.endpoint)
}

resp, err := tester.Client.Do(req)
if err != nil {
t.Fatalf("request failed: %s for endpoint %s", err, tc.endpoint)
}
if resp.StatusCode != tc.status {
t.Fatalf("unexpected status code for %s: got %d, want %d", tc.endpoint, resp.StatusCode, tc.status)
}

body, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
switch tc.bodyType {
case 0, 1:
if err == nil {
t.Fatalf("expected error reading body for %s, got none", tc.endpoint)
}
case 2:
if err != nil {
t.Fatalf("error reading body for %s: %s", tc.endpoint, err)
}
}
if string(body) != tc.body {
t.Fatalf("unexpected body for %s: got %q, want %q", tc.endpoint, string(body), tc.body)
}
}
}
4 changes: 4 additions & 0 deletions modules/caddyhttp/reverseproxy/reverseproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,10 @@ func (h *Handler) finalizeResponse(
if c := logger.Check(zapcore.WarnLevel, "aborting with incomplete response"); c != nil {
c.Write(zap.Error(err))
}
// flush the buffer to ensure the client sees the partial response
// see: https://github.com/caddyserver/caddy/issues/7845
//nolint:bodyclose
http.NewResponseController(rw).Flush()
// no extra logging from stdlib
panic(http.ErrAbortHandler)
}
Expand Down
Loading