Skip to content

Commit

Permalink
reverseproxy: Enable changing only the status code (close #2920)
Browse files Browse the repository at this point in the history
  • Loading branch information
mholt committed Jun 4, 2020
1 parent 7b0962b commit 7a99835
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 33 deletions.
39 changes: 39 additions & 0 deletions modules/caddyhttp/caddyhttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,45 @@ var errorEmptyHandler Handler = HandlerFunc(func(w http.ResponseWriter, r *http.
return nil
})

// ResponseHandler pairs a response matcher with custom handling
// logic. Either the status code can be changed to something else
// while using the original response body, or, if a status code
// is not set, it can execute a custom route list; this is useful
// for executing handler routes based on the properties of an HTTP
// response that has not been written out to the client yet.
//
// To use this type, provision it at module load time, then when
// ready to use, match the response against its matcher; if it
// matches (or doesn't have a matcher), change the status code on
// the response if configured; otherwise invoke the routes by
// calling `rh.Routes.Compile(next).ServeHTTP(rw, req)` (or similar).
type ResponseHandler struct {
// The response matcher for this handler. If empty/nil,
// it always matches.
Match *ResponseMatcher `json:"match,omitempty"`

// To write the original response body but with a different
// status code, set this field to the desired status code.
// If set, this takes priority over routes.
StatusCode WeakString `json:"status_code,omitempty"`

// The list of HTTP routes to execute if no status code is
// specified. If evaluated, the original response body
// will not be written.
Routes RouteList `json:"routes,omitempty"`
}

// Provision sets up the routse in rh.
func (rh *ResponseHandler) Provision(ctx caddy.Context) error {
if rh.Routes != nil {
err := rh.Routes.Provision(ctx)
if err != nil {
return err
}
}
return nil
}

// WeakString is a type that unmarshals any JSON value
// as a string literal, with the following exceptions:
//
Expand Down
24 changes: 21 additions & 3 deletions modules/caddyhttp/reverseproxy/reverseproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"net"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -531,15 +532,32 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
}
}

// see if any response handler is configured for this response from the backend
for i, rh := range h.HandleResponse {
if len(rh.Routes) == 0 {
if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) {
continue
}
if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) {

repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)

// if configured to only change the status code, do that then continue regular proxy response
if statusCodeStr := rh.StatusCode.String(); statusCodeStr != "" {
statusCode, err := strconv.Atoi(repl.ReplaceAll(statusCodeStr, ""))
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
if statusCode != 0 {
res.StatusCode = statusCode
}
break
}

// otherwise, if there are any routes configured, execute those as the
// actual response instead of what we got from the proxy backend
if len(rh.Routes) == 0 {
continue
}
res.Body.Close()
repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
repl.Set("http.reverse_proxy.status_code", res.StatusCode)
repl.Set("http.reverse_proxy.status_text", res.Status)
h.logger.Debug("handling response", zap.Int("handler", i))
Expand Down
30 changes: 0 additions & 30 deletions modules/caddyhttp/subroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,36 +80,6 @@ func (sr *Subroute) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handl
return err
}

// ResponseHandler pairs a response matcher with a route list.
// It is useful for executing handler routes based on the
// properties of an HTTP response that has not been written
// out to the client yet.
//
// To use this type, provision it at module load time, then
// when ready to use, match the response against its matcher;
// if it matches (or doesn't have a matcher), invoke the routes
// by calling `rh.Routes.Compile(next).ServeHTTP(rw, req)` (or
// similar).
type ResponseHandler struct {
// The response matcher for this handler. If empty/nil,
// it always matches.
Match *ResponseMatcher `json:"match,omitempty"`

// The list of HTTP routes to execute.
Routes RouteList `json:"routes,omitempty"`
}

// Provision sets up the routse in rh.
func (rh *ResponseHandler) Provision(ctx caddy.Context) error {
if rh.Routes != nil {
err := rh.Routes.Provision(ctx)
if err != nil {
return err
}
}
return nil
}

// Interface guards
var (
_ caddy.Provisioner = (*Subroute)(nil)
Expand Down

0 comments on commit 7a99835

Please sign in to comment.