fileserver: Add status code override (#4076)

After reading a question about the `handle_response` feature of `reverse_proxy`, I realized that we didn't have a way of serving an arbitrary file with a status code other than 200. This is an issue in situations where you want to serve a custom error page in routes that are not errors, like the aforementioned `handle_response`, where you may want to retain the status code returned by the proxy but write a response with content from a file.

This feature is super simple, basically if a status code is configured (can be a status code number, or a placeholder string) then that status will be written out before serving the file - if we write the status code first, then the stdlib won't write its own (only the first HTTP status header wins).
This commit is contained in:
Francis Lavoie 2021-04-08 13:09:12 -04:00 committed by GitHub
parent 45fb7202ac
commit 3f6283b385
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 140 additions and 0 deletions

View file

@ -0,0 +1,112 @@
localhost
root * /srv
handle /nope* {
file_server {
status 403
}
}
handle /custom-status* {
file_server {
status {env.CUSTOM_STATUS}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"status_code": "{env.CUSTOM_STATUS}"
}
]
}
]
}
],
"match": [
{
"path": [
"/custom-status*"
]
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "file_server",
"hide": [
"./Caddyfile"
],
"status_code": 403
}
]
}
]
}
],
"match": [
{
"path": [
"/nope*"
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}

View file

@ -40,6 +40,7 @@ func init() {
// index <files...> // index <files...>
// browse [<template_file>] // browse [<template_file>]
// precompressed <formats...> // precompressed <formats...>
// status <status>
// } // }
// //
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
@ -65,21 +66,25 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
if len(fsrv.Hide) == 0 { if len(fsrv.Hide) == 0 {
return nil, h.ArgErr() return nil, h.ArgErr()
} }
case "index": case "index":
fsrv.IndexNames = h.RemainingArgs() fsrv.IndexNames = h.RemainingArgs()
if len(fsrv.IndexNames) == 0 { if len(fsrv.IndexNames) == 0 {
return nil, h.ArgErr() return nil, h.ArgErr()
} }
case "root": case "root":
if !h.Args(&fsrv.Root) { if !h.Args(&fsrv.Root) {
return nil, h.ArgErr() return nil, h.ArgErr()
} }
case "browse": case "browse":
if fsrv.Browse != nil { if fsrv.Browse != nil {
return nil, h.Err("browsing is already configured") return nil, h.Err("browsing is already configured")
} }
fsrv.Browse = new(Browse) fsrv.Browse = new(Browse)
h.Args(&fsrv.Browse.TemplateFile) h.Args(&fsrv.Browse.TemplateFile)
case "precompressed": case "precompressed":
var order []string var order []string
for h.NextArg() { for h.NextArg() {
@ -100,6 +105,13 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
order = append(order, h.Val()) order = append(order, h.Val())
} }
fsrv.PrecompressedOrder = order fsrv.PrecompressedOrder = order
case "status":
if !h.NextArg() {
return nil, h.ArgErr()
}
fsrv.StatusCode = caddyhttp.WeakString(h.Val())
default: default:
return nil, h.Errf("unknown subdirective '%s'", h.Val()) return nil, h.Errf("unknown subdirective '%s'", h.Val())
} }

View file

@ -75,6 +75,12 @@ type FileServer struct {
// remove trailing slash from URIs for files. Default is true. // remove trailing slash from URIs for files. Default is true.
CanonicalURIs *bool `json:"canonical_uris,omitempty"` CanonicalURIs *bool `json:"canonical_uris,omitempty"`
// Override the status code written when successfully serving a file.
// Particularly useful when explicitly serving a file as display for
// an error, like a 404 page. A placeholder may be used. By default,
// the status code will typically be 200, or 206 for partial content.
StatusCode caddyhttp.WeakString `json:"status_code,omitempty"`
// If pass-thru mode is enabled and a requested file is not found, // If pass-thru mode is enabled and a requested file is not found,
// it will invoke the next handler in the chain instead of returning // it will invoke the next handler in the chain instead of returning
// a 404 error. By default, this is false (disabled). // a 404 error. By default, this is false (disabled).
@ -345,6 +351,16 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
return nil return nil
} }
// if a status code override is configured, write the status code
// before serving the file
if codeStr := fsrv.StatusCode.String(); codeStr != "" {
intVal, err := strconv.Atoi(repl.ReplaceAll(codeStr, ""))
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
w.WriteHeader(intVal)
}
// let the standard library do what it does best; note, however, // let the standard library do what it does best; note, however,
// that errors generated by ServeContent are written immediately // that errors generated by ServeContent are written immediately
// to the response, so we cannot handle them (but errors there // to the response, so we cannot handle them (but errors there