From 907e2d8d3a8ebaf14aa99939c493e56645bc2089 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Fri, 17 Sep 2021 02:52:32 -0400 Subject: [PATCH] caddyhttp: Add support for triggering errors from `try_files` (#4346) * caddyhttp: Add support for triggering errors from `try_files` * caddyhttp: Use vars instead of placeholders/replacer for matcher errors * caddyhttp: Add comment for matcher error var key --- modules/caddyhttp/fileserver/matcher.go | 25 ++++++++++++++++++++++++- modules/caddyhttp/matchers.go | 6 ++++++ modules/caddyhttp/routes.go | 9 +++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index 739b9e05..f8e9ce0f 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -19,6 +19,7 @@ import ( "net/http" "os" "path" + "strconv" "strings" "time" @@ -60,7 +61,11 @@ type MatchFile struct { // directories are treated distinctly, so to match // a directory, the filepath MUST end in a forward // slash `/`. To match a regular file, there must - // be no trailing slash. Accepts placeholders. + // be no trailing slash. Accepts placeholders. If + // the policy is "first_exist", then an error may + // be triggered as a fallback by configuring "=" + // followed by a status code number, + // for example "=404". TryFiles []string `json:"try_files,omitempty"` // How to choose a file in TryFiles. Can be: @@ -205,6 +210,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { switch m.TryPolicy { case "", tryPolicyFirstExist: for _, f := range m.TryFiles { + if err := parseErrorCode(f); err != nil { + caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err) + return + } suffix, fullpath, remainder := prepareFilePath(f) if info, exists := strictFileExists(fullpath); exists { setPlaceholders(info, suffix, fullpath, remainder) @@ -274,6 +283,20 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { return } +// parseErrorCode checks if the input is a status +// code number, prefixed by "=", and returns an +// error if so. +func parseErrorCode(input string) error { + if len(input) > 1 && input[0] == '=' { + code, err := strconv.Atoi(input[1:]) + if err != nil || code < 100 || code > 999 { + return nil + } + return caddyhttp.Error(code, fmt.Errorf("%s", input[1:])) + } + return nil +} + // strictFileExists returns true if file exists // and matches the convention of the given file // path. If the path ends in a forward slash, diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index b452d48e..a22a3f1d 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -987,6 +987,12 @@ var wordRE = regexp.MustCompile(`\w+`) const regexpPlaceholderPrefix = "http.regexp" +// MatcherErrorVarKey is the key used for the variable that +// holds an optional error emitted from a request matcher, +// to short-circuit the handler chain, since matchers cannot +// return errors via the RequestMatcher interface. +const MatcherErrorVarKey = "matchers.error" + // Interface guards var ( _ RequestMatcher = (*MatchHost)(nil) diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index ebd763c7..7b2871ff 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -200,6 +200,15 @@ func wrapRoute(route Route) Middleware { // route must match at least one of the matcher sets if !route.MatcherSets.AnyMatch(req) { + // allow matchers the opportunity to short circuit + // the request and trigger the error handling chain + err, ok := GetVar(req.Context(), MatcherErrorVarKey).(error) + if ok { + return err + } + + // call the next handler, and skip this one, + // since the matcher didn't match return nextCopy.ServeHTTP(rw, req) }