diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 276616b4..cc6b530c 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -109,6 +109,11 @@ type Handler struct { // response is recognized as a streaming response, or if its // content length is -1; for such responses, writes are flushed // to the client immediately. + // + // Normally, a request will be canceled if the client disconnects + // before the response is received from the backend. If explicitly + // set to -1, client disconnection will be ignored and the request + // will be completed to help facilitate low-latency streaming. FlushInterval caddy.Duration `json:"flush_interval,omitempty"` // A list of IP ranges (supports CIDR notation) from which @@ -757,6 +762,15 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) } + // if FlushInterval is explicitly configured to -1 (i.e. flush continuously to achieve + // low-latency streaming), don't let the transport cancel the request if the client + // disconnects: user probably wants us to finish sending the data to the upstream + // regardless, and we should expect client disconnection in low-latency streaming + // scenarios (see issue #4922) + if h.FlushInterval == -1 { + req = req.WithContext(ignoreClientGoneContext{req.Context(), h.ctx.Done()}) + } + // do the round-trip; emit debug log with values we know are // safe, or if there is no error, emit fuller log entry start := time.Now() @@ -1374,6 +1388,19 @@ type handleResponseContext struct { isFinalized bool } +// ignoreClientGoneContext is a special context.Context type +// intended for use when doing a RoundTrip where you don't +// want a client disconnection to cancel the request during +// the roundtrip. Set its done field to a Done() channel +// of a context that doesn't get canceled when the client +// disconnects, such as caddy.Context.Done() instead. +type ignoreClientGoneContext struct { + context.Context + done <-chan struct{} +} + +func (c ignoreClientGoneContext) Done() <-chan struct{} { return c.done } + // proxyHandleResponseContextCtxKey is the context key for the active proxy handler // so that handle_response routes can inherit some config options // from the proxy handler.