mirror of
https://github.com/caddyserver/caddy.git
synced 2025-03-24 22:24:54 +01:00
caddyhttp: Fix fallback for the error handler chain
This is mainly a problem with the default behaviour of the Caddyfile's `handle_errors` routes, but it's kinda tricky to solve well. I went with an approach that involves a smidge of magic which might not really be desirable. See https://caddy.community/t/problem-with-basicauth-handle-errors/12243/9 for context. So we do already have a default fallback for error routes, i.e. `errorEmptyHandler` in `caddyhttp.go` which is always configured as the last handler in the chain (implicitly, not visible in the config). The problem is that when subroutes come into play with `"terminal": true`, then this fallback handler will never be reached. This is the case when the Caddyfile generates a config which has a host matcher from a site block (which is most of the time) when the user configured `handle_errors` to handle specific errors (such as 502s or 404s to serve HTML pages for those, etc). If other errors, like `basicauth`'s 401s are emitted in that case, then the result is that the default of HTTP status 200 will be served instead of the 401, which breaks `basicauth` completely. The fix I went with is to make the Caddyfile adapter append special `error` handlers inside of the `handle_errors` subroutes which throw error `-1`, which `server.go` then picks up, and seeing `-1` responds with the original error code of `401` instead. The `-1` thing is the aforementioned magic. At first, I had this implemented with `static_response` setting the StatusCode to `{http.error.status_code}`, but it didn't feel right to use a placeholder because it's inherently slightly less efficient, and it wasn't 100% correct because non-handler errors wouldn't be handled as 500s properly I think (because if it's not a `HandlerError`, then `http.error.status_code` doesn't exist, so it would maybe try to write an the placeholder replacement result of an empty string as `0` for the status code).
This commit is contained in:
parent
74f5d66c48
commit
95b6ac44a6
3 changed files with 152 additions and 26 deletions
|
@ -617,7 +617,7 @@ func (st *ServerType) serversFromPairings(
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the site block's route(s) to the server
|
// add the site block's route(s) to the server
|
||||||
srv.Routes = appendSubrouteToRouteList(srv.Routes, siteSubroute, matcherSetsEnc, p, warnings)
|
srv.Routes = appendSubrouteToRouteList(srv.Routes, siteSubroute, matcherSetsEnc, p, false, warnings)
|
||||||
|
|
||||||
// if error routes are defined, add those too
|
// if error routes are defined, add those too
|
||||||
if errorSubrouteVals, ok := sblock.pile["error_route"]; ok {
|
if errorSubrouteVals, ok := sblock.pile["error_route"]; ok {
|
||||||
|
@ -626,7 +626,7 @@ func (st *ServerType) serversFromPairings(
|
||||||
}
|
}
|
||||||
for _, val := range errorSubrouteVals {
|
for _, val := range errorSubrouteVals {
|
||||||
sr := val.Value.(*caddyhttp.Subroute)
|
sr := val.Value.(*caddyhttp.Subroute)
|
||||||
srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, warnings)
|
srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, true, warnings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -904,6 +904,7 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
|
||||||
subroute *caddyhttp.Subroute,
|
subroute *caddyhttp.Subroute,
|
||||||
matcherSetsEnc []caddy.ModuleMap,
|
matcherSetsEnc []caddy.ModuleMap,
|
||||||
p sbAddrAssociation,
|
p sbAddrAssociation,
|
||||||
|
isError bool,
|
||||||
warnings *[]caddyconfig.Warning) caddyhttp.RouteList {
|
warnings *[]caddyconfig.Warning) caddyhttp.RouteList {
|
||||||
|
|
||||||
// nothing to do if... there's nothing to do
|
// nothing to do if... there's nothing to do
|
||||||
|
@ -930,6 +931,20 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
|
||||||
caddyconfig.JSONModuleObject(subroute, "handler", "subroute", warnings),
|
caddyconfig.JSONModuleObject(subroute, "handler", "subroute", warnings),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for error subroutes, we need to append a fallback static error
|
||||||
|
// because the default fallback won't be reached due to the route
|
||||||
|
// being marked as terminal. Using -1 as a special value to tell
|
||||||
|
// the handling in server.go to write the original error status instead.
|
||||||
|
if isError {
|
||||||
|
route.HandlersRaw = append(
|
||||||
|
route.HandlersRaw,
|
||||||
|
caddyconfig.JSONModuleObject(caddyhttp.StaticError{
|
||||||
|
StatusCode: caddyhttp.WeakString(strconv.Itoa(caddyhttp.WriteOriginalErrorCode)),
|
||||||
|
}, "handler", "error", warnings),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 {
|
if len(route.MatcherSetsRaw) > 0 || len(route.HandlersRaw) > 0 {
|
||||||
routeList = append(routeList, route)
|
routeList = append(routeList, route)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
http://127.0.0.1 {
|
||||||
|
handle_errors {
|
||||||
|
@404 expression `{http.error.status_code} == 404`
|
||||||
|
respond @404 "Not Found"
|
||||||
|
}
|
||||||
|
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"127.0.0.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": {
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"127.0.0.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Not Found",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} == 404"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -233,35 +233,52 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// add HTTP error information to request context
|
// add HTTP error information to request context
|
||||||
r = s.Errors.WithError(r, err)
|
r = s.Errors.WithError(r, err)
|
||||||
|
|
||||||
if s.Errors != nil && len(s.Errors.Routes) > 0 {
|
// if there's no error routes, then just write out the error status
|
||||||
// execute user-defined error handling route
|
if s.Errors == nil || len(s.Errors.Routes) == 0 {
|
||||||
err2 := s.errorHandlerChain.ServeHTTP(w, r)
|
|
||||||
if err2 == nil {
|
|
||||||
// user's error route handled the error response
|
|
||||||
// successfully, so now just log the error
|
|
||||||
if errStatus >= 500 {
|
if errStatus >= 500 {
|
||||||
logger.Error(errMsg, errFields...)
|
logger.Error(errMsg, errFields...)
|
||||||
}
|
}
|
||||||
} else {
|
w.WriteHeader(errStatus)
|
||||||
// well... this is awkward
|
return
|
||||||
errFields = append([]zapcore.Field{
|
}
|
||||||
|
|
||||||
|
// execute user-defined error handling route
|
||||||
|
err2 := s.errorHandlerChain.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
// if the error handlers emit an error, it's usually
|
||||||
|
// a problem, but in some cases it's intended
|
||||||
|
if err2 != nil {
|
||||||
|
// if the error was WriteOriginalErrorCode, then it was meant as a signal
|
||||||
|
// from the error routes to just write the original error
|
||||||
|
// see https://caddy.community/t/problem-with-basicauth-handle-errors/12243/9
|
||||||
|
if handlerErr, ok := err2.(HandlerError); ok && handlerErr.StatusCode == WriteOriginalErrorCode {
|
||||||
|
if errStatus >= 500 {
|
||||||
|
logger.Error(errMsg, errFields...)
|
||||||
|
}
|
||||||
|
w.WriteHeader(errStatus)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// well... this is awkward, the error handler
|
||||||
|
// returned an unexpected error
|
||||||
|
logger.Error("error handling handler error", append([]zapcore.Field{
|
||||||
zap.String("error", err2.Error()),
|
zap.String("error", err2.Error()),
|
||||||
zap.Namespace("first_error"),
|
zap.Namespace("first_error"),
|
||||||
zap.String("msg", errMsg),
|
zap.String("msg", errMsg),
|
||||||
}, errFields...)
|
}, errFields...)...)
|
||||||
logger.Error("error handling handler error", errFields...)
|
|
||||||
if handlerErr, ok := err.(HandlerError); ok {
|
if handlerErr, ok := err.(HandlerError); ok {
|
||||||
w.WriteHeader(handlerErr.StatusCode)
|
w.WriteHeader(handlerErr.StatusCode)
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
// user's error route handled the error response
|
||||||
|
// successfully, so now just log the error if necessary
|
||||||
if errStatus >= 500 {
|
if errStatus >= 500 {
|
||||||
logger.Error(errMsg, errFields...)
|
logger.Error(errMsg, errFields...)
|
||||||
}
|
}
|
||||||
w.WriteHeader(errStatus)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrapPrimaryRoute wraps stack (a compiled middleware handler chain)
|
// wrapPrimaryRoute wraps stack (a compiled middleware handler chain)
|
||||||
|
@ -615,6 +632,11 @@ const (
|
||||||
|
|
||||||
// commonLogEmptyValue is the common empty log value.
|
// commonLogEmptyValue is the common empty log value.
|
||||||
commonLogEmptyValue = "-"
|
commonLogEmptyValue = "-"
|
||||||
|
|
||||||
|
// WriteOriginalErrorCode is the special error code to use to signal
|
||||||
|
// the server error handling logic to write the original error code
|
||||||
|
// it received as a fallback if the error routes don't handle the error.
|
||||||
|
WriteOriginalErrorCode = -1
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context keys for HTTP request context values.
|
// Context keys for HTTP request context values.
|
||||||
|
|
Loading…
Add table
Reference in a new issue