summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/reverseproxy/reverseproxy.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp/reverseproxy/reverseproxy.go')
-rw-r--r--modules/caddyhttp/reverseproxy/reverseproxy.go87
1 files changed, 82 insertions, 5 deletions
diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go
index 3355f0b..2131a91 100644
--- a/modules/caddyhttp/reverseproxy/reverseproxy.go
+++ b/modules/caddyhttp/reverseproxy/reverseproxy.go
@@ -790,12 +790,33 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
h.logger.Debug("handling response", zap.Int("handler", i))
+ // we make some data available via request context to child routes
+ // so that they may inherit some options and functions from the
+ // handler, and be able to copy the response.
+ hrc := &handleResponseContext{
+ handler: h,
+ response: res,
+ start: start,
+ logger: logger,
+ }
+ ctx := req.Context()
+ ctx = context.WithValue(ctx, proxyHandleResponseContextCtxKey, hrc)
+
// pass the request through the response handler routes
- routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req)
+ routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req.WithContext(ctx))
+
+ // if the response handler routes already finalized the response,
+ // we can return early. It should be finalized if the routes executed
+ // included a copy_response handler. If a fresh response was written
+ // by the routes instead, then we still need to finalize the response
+ // without copying the body.
+ if routeErr == nil && hrc.isFinalized {
+ return nil
+ }
- // always close the response body afterwards since it's expected
+ // always close the response body afterwards, since it's expected
// that the response handler routes will have written to the
- // response writer with a new body
+ // response writer with a new body, if it wasn't already finalized.
res.Body.Close()
bodyClosed = true
@@ -804,8 +825,25 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
// the roundtrip was successful and to not retry
return roundtripSucceeded{routeErr}
}
+
+ // we've already closed the body, so there's no use allowing
+ // another response handler to run as well
+ break
}
+ return h.finalizeResponse(rw, req, res, repl, start, logger, bodyClosed)
+}
+
+// finalizeResponse prepares and copies the response.
+func (h Handler) finalizeResponse(
+ rw http.ResponseWriter,
+ req *http.Request,
+ res *http.Response,
+ repl *caddy.Replacer,
+ start time.Time,
+ logger *zap.Logger,
+ bodyClosed bool,
+) error {
// deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
if res.StatusCode == http.StatusSwitchingProtocols {
h.handleUpgradeResponse(logger, rw, req, res)
@@ -818,6 +856,13 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
res.Header.Del(h)
}
+ // remove the content length if we're not going to be copying
+ // from the response, because otherwise there'll be a mismatch
+ // between bytes written and the advertised length
+ if bodyClosed {
+ res.Header.Del("Content-Length")
+ }
+
// apply any response header operations
if h.Headers != nil && h.Headers.Response != nil {
if h.Headers.Response.Require == nil ||
@@ -841,7 +886,7 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
rw.WriteHeader(res.StatusCode)
if !bodyClosed {
- err = h.copyResponse(rw, res.Body, h.flushInterval(req, res))
+ err := h.copyResponse(rw, res.Body, h.flushInterval(req, res))
res.Body.Close() // close now, instead of defer, to populate res.Trailer
if err != nil {
// we're streaming the response and we've already written headers, so
@@ -863,7 +908,7 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
}
// total duration spent proxying, including writing response body
- repl.Set("http.reverse_proxy.upstream.duration", duration)
+ repl.Set("http.reverse_proxy.upstream.duration", time.Since(start))
if len(res.Trailer) == announcedTrailers {
copyHeader(rw.Header(), res.Trailer)
@@ -1227,6 +1272,38 @@ var bufPool = sync.Pool{
},
}
+// handleResponseContext carries some contextual information about the
+// the current proxy handling.
+type handleResponseContext struct {
+ // handler is the active proxy handler instance, so that
+ // routes like copy_response may inherit some config
+ // options and have access to handler methods.
+ handler *Handler
+
+ // response is the actual response received from the proxy
+ // roundtrip, to potentially be copied if a copy_response
+ // handler is in the handle_response routes.
+ response *http.Response
+
+ // start is the time just before the proxy roundtrip was
+ // performed, used for logging.
+ start time.Time
+
+ // logger is the prepared logger which is used to write logs
+ // with the request, duration, and selected upstream attached.
+ logger *zap.Logger
+
+ // isFinalized is whether the response has been finalized,
+ // i.e. copied and closed, to make sure that it doesn't
+ // happen twice.
+ isFinalized bool
+}
+
+// proxyHandleResponseContextCtxKey is the context key for the active proxy handler
+// so that handle_response routes can inherit some config options
+// from the proxy handler.
+const proxyHandleResponseContextCtxKey caddy.CtxKey = "reverse_proxy_handle_response_context"
+
// Interface guards
var (
_ caddy.Provisioner = (*Handler)(nil)