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
This commit is contained in:
Francis Lavoie 2021-09-17 02:52:32 -04:00 committed by GitHub
parent 33c70f418f
commit 907e2d8d3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 39 additions and 1 deletions

View file

@ -19,6 +19,7 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"strconv"
"strings" "strings"
"time" "time"
@ -60,7 +61,11 @@ type MatchFile struct {
// directories are treated distinctly, so to match // directories are treated distinctly, so to match
// a directory, the filepath MUST end in a forward // a directory, the filepath MUST end in a forward
// slash `/`. To match a regular file, there must // 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"` TryFiles []string `json:"try_files,omitempty"`
// How to choose a file in TryFiles. Can be: // 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 { switch m.TryPolicy {
case "", tryPolicyFirstExist: case "", tryPolicyFirstExist:
for _, f := range m.TryFiles { for _, f := range m.TryFiles {
if err := parseErrorCode(f); err != nil {
caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
return
}
suffix, fullpath, remainder := prepareFilePath(f) suffix, fullpath, remainder := prepareFilePath(f)
if info, exists := strictFileExists(fullpath); exists { if info, exists := strictFileExists(fullpath); exists {
setPlaceholders(info, suffix, fullpath, remainder) setPlaceholders(info, suffix, fullpath, remainder)
@ -274,6 +283,20 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
return 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 // strictFileExists returns true if file exists
// and matches the convention of the given file // and matches the convention of the given file
// path. If the path ends in a forward slash, // path. If the path ends in a forward slash,

View file

@ -987,6 +987,12 @@ var wordRE = regexp.MustCompile(`\w+`)
const regexpPlaceholderPrefix = "http.regexp" 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 // Interface guards
var ( var (
_ RequestMatcher = (*MatchHost)(nil) _ RequestMatcher = (*MatchHost)(nil)

View file

@ -200,6 +200,15 @@ func wrapRoute(route Route) Middleware {
// route must match at least one of the matcher sets // route must match at least one of the matcher sets
if !route.MatcherSets.AnyMatch(req) { 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) return nextCopy.ServeHTTP(rw, req)
} }