Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
reverseproxy: Enable response interception (#1447, #2920)
It's a raw, low-level implementation for now, but it's very flexible. More sugar-coating can be added after error handling is more developed.
Matthew Holt committed
May 27, 2020 at 10:15 UTC
538ddb85876001976fa0c76f10b912c0870fe6d7
2 files changed
+87
-15
modules/caddyhttp/reverseproxy/reverseproxy.go
+57
-15
@@ -94,6 +94,20 @@ type Handler struct {
94
// be avoided if at all possible for performance reasons.
95
BufferRequests bool `json:"buffer_requests,omitempty"`
96
97
+ // List of handlers and their associated matchers to evaluate
98
+ // after successful roundtrips. The first handler that matches
99
+ // the response from a backend will be invoked. The response
100
+ // body from the backend will not be written to the client;
101
+ // it is up to the handler to finish handling the response.
102
+ // If passive health checks are enabled, any errors from the
103
+ // handler chain will not affect the health status of the
104
+ // backend.
105
+ //
106
+ // Two new placeholders are available in this handler chain:
107
+ // - `{http.reverse_proxy.status_code}` The status code
108
+ // - `{http.reverse_proxy.status_text}` The status text
109
+ HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"`
110
+
111
Transport http.RoundTripper `json:"-"`
112
CB CircuitBreaker `json:"-"`
113
@@ -252,6 +266,14 @@ func (h *Handler) Provision(ctx caddy.Context) error {
266
go h.activeHealthChecker()
267
}
268
269
+ // set up any response routes
270
+ for i, rh := range h.HandleResponse {
271
+ err := rh.Provision(ctx)
272
+ if err != nil {
273
+ return fmt.Errorf("provisioning response handler %d: %v", i, err)
274
+ }
275
+ }
276
+
277
return nil
278
}
279
@@ -361,13 +383,21 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
383
}
384
385
// proxy the request to that upstream
364
- proxyErr = h.reverseProxy(w, r, dialInfo)
386
+ proxyErr = h.reverseProxy(w, r, dialInfo, next)
387
if proxyErr == nil || proxyErr == context.Canceled {
388
// context.Canceled happens when the downstream client
389
// cancels the request, which is not our failure
390
return nil
391
}
392
393
+ // if the roundtrip was successful, don't retry the request or
394
+ // ding the health status of the upstream (an error can still
395
+ // occur after the roundtrip if, for example, a response handler
396
+ // after the roundtrip returns an error)
397
+ if succ, ok := proxyErr.(roundtripSucceeded); ok {
398
+ return succ.error
399
+ }
400
+
401
// remember this failure (if enabled)
402
h.countFailure(upstream)
403
@@ -456,7 +486,7 @@ func (h Handler) prepareRequest(req *http.Request) error {
486
// reverseProxy performs a round-trip to the given backend and processes the response with the client.
487
// (This method is mostly the beginning of what was borrowed from the net/http/httputil package in the
488
// Go standard library which was used as the foundation.)
459
-func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di DialInfo) error {
489
+func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di DialInfo, next caddyhttp.Handler) error {
490
di.Upstream.Host.CountRequest(1)
491
defer di.Upstream.Host.CountRequest(-1)
492
@@ -471,16 +501,14 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
501
logger := h.logger.With(
502
zap.String("upstream", di.Upstream.String()),
503
zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: req}),
474
- zap.Duration("duration", duration),
475
- )
504
+ zap.Duration("duration", duration))
505
if err != nil {
506
logger.Debug("upstream roundtrip", zap.Error(err))
507
return err
508
}
509
logger.Debug("upstream roundtrip",
510
zap.Object("headers", caddyhttp.LoggableHTTPHeader(res.Header)),
482
- zap.Int("status", res.StatusCode),
483
- )
511
+ zap.Int("status", res.StatusCode))
512
513
// update circuit breaker on current conditions
514
if di.Upstream.cb != nil {
@@ -503,6 +531,25 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
531
}
532
}
533
534
+ for i, rh := range h.HandleResponse {
535
+ if len(rh.Routes) == 0 {
536
+ continue
537
+ }
538
+ if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) {
539
+ continue
540
+ }
541
+ res.Body.Close()
542
+ repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
543
+ repl.Set("http.reverse_proxy.status_code", res.StatusCode)
544
+ repl.Set("http.reverse_proxy.status_text", res.Status)
545
+ h.logger.Debug("handling response", zap.Int("handler", i))
546
+ if routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req); routeErr != nil {
547
+ // wrap error in roundtripSucceeded so caller knows that
548
+ // the roundtrip was successful and to not retry
549
+ return roundtripSucceeded{routeErr}
550
+ }
551
+ }
552
+
553
// Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
554
if res.StatusCode == http.StatusSwitchingProtocols {
555
h.handleUpgradeResponse(rw, req, res)
@@ -537,15 +584,6 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
584
rw.Header().Add("Trailer", strings.Join(trailerKeys, ", "))
585
}
586
540
- // TODO: there should be an option to return an error if the response
541
- // matches some criteria; would solve https://github.com/caddyserver/caddy/issues/1447
542
- // by allowing the backend to determine whether this server should treat
543
- // a 400+ status code as an error -- but we might need to be careful that
544
- // we do not affect the health status of the backend... still looking into
545
- // that; if we need to avoid that, we should return a particular error type
546
- // that the caller of this function checks for and only applies health
547
- // status changes if the error is not this special type
548
-
587
rw.WriteHeader(res.StatusCode)
588
589
err = h.copyResponse(rw, res.Body, h.flushInterval(req, res))
@@ -782,6 +820,10 @@ type TLSTransport interface {
820
EnableTLS(base *TLSConfig) error
821
}
822
823
+// roundtripSucceeded is an error type that is returned if the
824
+// roundtrip succeeded, but an error occurred after-the-fact.
825
+type roundtripSucceeded struct{ error }
826
+
827
var bufPool = sync.Pool{
828
New: func() interface{} {
829
return new(bytes.Buffer)
modules/caddyhttp/subroute.go
+30
@@ -80,6 +80,36 @@ func (sr *Subroute) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handl
80
return err
81
}
82
83
+// ResponseHandler pairs a response matcher with a route list.
84
+// It is useful for executing handler routes based on the
85
+// properties of an HTTP response that has not been written
86
+// out to the client yet.
87
+//
88
+// To use this type, provision it at module load time, then
89
+// when ready to use, match the response against its matcher;
90
+// if it matches (or doesn't have a matcher), invoke the routes
91
+// by calling `rh.Routes.Compile(next).ServeHTTP(rw, req)` (or
92
+// similar).
93
+type ResponseHandler struct {
94
+ // The response matcher for this handler. If empty/nil,
95
+ // it always matches.
96
+ Match *ResponseMatcher `json:"match,omitempty"`
97
+
98
+ // The list of HTTP routes to execute.
99
+ Routes RouteList `json:"routes,omitempty"`
100
+}
101
+
102
+// Provision sets up the routse in rh.
103
+func (rh *ResponseHandler) Provision(ctx caddy.Context) error {
104
+ if rh.Routes != nil {
105
+ err := rh.Routes.Provision(ctx)
106
+ if err != nil {
107
+ return err
108
+ }
109
+ }
110
+ return nil
111
+}
112
+
113
// Interface guards
114
var (
115
_ caddy.Provisioner = (*Subroute)(nil)