diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 74cca4a4f..2cd3114c8 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -886,12 +886,14 @@ func (st *ServerType) serversFromPairings( if srv.Errors == nil { srv.Errors = new(caddyhttp.HTTPErrorConfig) } + // Move catch-all handle_errors (no matchers) to the end so + // status-specific blocks get a chance to match first; otherwise + // preserve the user's declaration order. sort.SliceStable(errorSubrouteVals, func(i, j int) bool { sri, srj := errorSubrouteVals[i].Value.(*caddyhttp.Subroute), errorSubrouteVals[j].Value.(*caddyhttp.Subroute) - if len(sri.Routes[0].MatcherSetsRaw) == 0 && len(srj.Routes[0].MatcherSetsRaw) != 0 { - return false - } - return true + iEmpty := len(sri.Routes[0].MatcherSetsRaw) == 0 + jEmpty := len(srj.Routes[0].MatcherSetsRaw) == 0 + return !iEmpty && jEmpty }) errorsSubroute := &caddyhttp.Subroute{} for _, val := range errorSubrouteVals { diff --git a/caddytest/integration/caddyfile_adapt/error_multi_site_blocks.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_multi_site_blocks.caddyfiletest index 1bec4b3e8..7888f148c 100644 --- a/caddytest/integration/caddyfile_adapt/error_multi_site_blocks.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/error_multi_site_blocks.caddyfiletest @@ -162,28 +162,6 @@ bar.localhost { { "handler": "subroute", "routes": [ - { - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "404 or 410 error", - "handler": "static_response" - } - ] - } - ] - } - ], - "match": [ - { - "expression": "{http.error.status_code} in [410]" - } - ] - }, { "handle": [ { @@ -205,6 +183,28 @@ bar.localhost { "expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599" } ] + }, + { + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "404 or 410 error", + "handler": "static_response" + } + ] + } + ] + } + ], + "match": [ + { + "expression": "{http.error.status_code} in [410]" + } + ] } ] } @@ -223,28 +223,6 @@ bar.localhost { { "handler": "subroute", "routes": [ - { - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "404 or 410 error from second site", - "handler": "static_response" - } - ] - } - ] - } - ], - "match": [ - { - "expression": "{http.error.status_code} in [410]" - } - ] - }, { "handle": [ { @@ -266,6 +244,28 @@ bar.localhost { "expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599" } ] + }, + { + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "404 or 410 error from second site", + "handler": "static_response" + } + ] + } + ] + } + ], + "match": [ + { + "expression": "{http.error.status_code} in [410]" + } + ] } ] } diff --git a/caddytest/integration/caddyfile_adapt/error_range_simple_codes.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_range_simple_codes.caddyfiletest index 9d4d2645c..87cbc2dd8 100644 --- a/caddytest/integration/caddyfile_adapt/error_range_simple_codes.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/error_range_simple_codes.caddyfiletest @@ -113,28 +113,6 @@ localhost:2099 { { "handler": "subroute", "routes": [ - { - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "Error in the [400 .. 499] range", - "handler": "static_response" - } - ] - } - ] - } - ], - "match": [ - { - "expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499" - } - ] - }, { "handle": [ { @@ -156,6 +134,28 @@ localhost:2099 { "expression": "{http.error.status_code} \u003e= 300 \u0026\u0026 {http.error.status_code} \u003c= 399 || {http.error.status_code} in [500]" } ] + }, + { + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Error in the [400 .. 499] range", + "handler": "static_response" + } + ] + } + ] + } + ], + "match": [ + { + "expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499" + } + ] } ] } diff --git a/caddytest/spec/http/handle_errors/spec.hurl b/caddytest/spec/http/handle_errors/spec.hurl new file mode 100644 index 000000000..54b4409aa --- /dev/null +++ b/caddytest/spec/http/handle_errors/spec.hurl @@ -0,0 +1,360 @@ +# Configure Caddy with handle_errors directive +POST http://localhost:2019/load +Content-Type: text/caddyfile +``` +{ + skip_install_trust + http_port 9080 + https_port 9443 + local_certs +} +localhost { + root * /nonexistent + file_server + handle_errors { + respond "Error {err.status_code}: {err.status_text}" + } +} +``` + +# handle_errors catches file_server 404 errors +GET https://localhost:9443/missing.txt +[Options] +insecure: true +HTTP 404 +[Asserts] +body == "Error 404: Not Found" + + +# Configure Caddy with handle_errors for specific status codes +POST http://localhost:2019/load +Content-Type: text/caddyfile +``` +{ + skip_install_trust + http_port 9080 + https_port 9443 + local_certs +} +localhost { + error /forbidden* "Forbidden" 403 + error /gone* "Gone" 410 + handle_errors 403 { + respond "Custom 403 handler" + } + handle_errors 4xx { + respond "Generic 4xx handler: {err.status_code}" + } +} +``` + +# handle_errors matches specific 403 handler +GET https://localhost:9443/forbidden +[Options] +insecure: true +HTTP 403 +[Asserts] +body == "Custom 403 handler" + + +# handle_errors uses 4xx handler for other client errors +GET https://localhost:9443/gone +[Options] +insecure: true +HTTP 410 +[Asserts] +body == "Generic 4xx handler: 410" + + +# Configure Caddy with handle_errors using templates +POST http://localhost:2019/load +Content-Type: text/caddyfile +``` +{ + skip_install_trust + http_port 9080 + https_port 9443 + local_certs +} +localhost { + root * /nonexistent + file_server + handle_errors { + respond "Path: {http.request.orig_uri.path}, Status: {err.status_code}" + } +} +``` + +# handle_errors has access to request context +GET https://localhost:9443/test/missing.html +[Options] +insecure: true +HTTP 404 +[Asserts] +body == "Path: /test/missing.html, Status: 404" + + +# Configure Caddy with nested error handling +POST http://localhost:2019/load +Content-Type: text/caddyfile +``` +{ + skip_install_trust + http_port 9080 + https_port 9443 + local_certs +} +localhost { + error /fail 500 + handle_errors 5xx { + respond "Server error occurred" + } + handle_errors 4xx { + respond "Client error occurred" + } +} +``` + +# handle_errors matches 5xx errors +GET https://localhost:9443/fail +[Options] +insecure: true +HTTP 500 +[Asserts] +body == "Server error occurred" + + +# Configure Caddy with handle_errors and error ID +POST http://localhost:2019/load +Content-Type: text/caddyfile +``` +{ + skip_install_trust + http_port 9080 + https_port 9443 + local_certs +} +localhost { + error /test "Test error" 400 + handle_errors { + respond "Error ID: {err.id}, Message: {err.message}" + } +} +``` + +# handle_errors exposes error ID and message +GET https://localhost:9443/test +[Options] +insecure: true +HTTP 400 +[Asserts] +body contains "Error ID:" +body contains "Message: Test error" + + +# Configure Caddy with handle_errors rewrite +POST http://localhost:2019/load +Content-Type: text/caddyfile +``` +{ + skip_install_trust + http_port 9080 + https_port 9443 + local_certs +} +localhost { + root * {{indexed_root}} + file_server + handle_errors 404 { + rewrite * /index.html + file_server + } +} +``` + +# handle_errors can rewrite and serve different file +GET https://localhost:9443/nonexistent.txt +[Options] +insecure: true +HTTP 404 +[Asserts] +body contains "Index.html" + + +# Configure Caddy with multiple status codes on a single handle_errors block +POST http://localhost:2019/load +Content-Type: text/caddyfile +``` +{ + skip_install_trust + http_port 9080 + https_port 9443 + local_certs +} +localhost { + error /a "A" 403 + error /b "B" 404 + error /c "C" 500 + handle_errors 403 404 { + respond "auth-or-missing: {err.status_code}" + } + handle_errors { + respond "other: {err.status_code}" + } +} +``` + +# multiple codes on one handle_errors block — first code +GET https://localhost:9443/a +[Options] +insecure: true +HTTP 403 +[Asserts] +body == "auth-or-missing: 403" + +# multiple codes on one handle_errors block — second code +GET https://localhost:9443/b +[Options] +insecure: true +HTTP 404 +[Asserts] +body == "auth-or-missing: 404" + +# catch-all handle_errors block matches unlisted codes (and stays last) +GET https://localhost:9443/c +[Options] +insecure: true +HTTP 500 +[Asserts] +body == "other: 500" + + +# Configure Caddy with a mix of single code and range on one block +POST http://localhost:2019/load +Content-Type: text/caddyfile +``` +{ + skip_install_trust + http_port 9080 + https_port 9443 + local_certs +} +localhost { + error /redir "Moved" 301 + error /server "Internal" 500 + error /client "Bad" 400 + handle_errors 500 3xx { + respond "redirect-or-server: {err.status_code}" + } + handle_errors 4xx { + respond "client: {err.status_code}" + } +} +``` + +# mixed-range handle_errors matches the single code (500) +GET https://localhost:9443/server +[Options] +insecure: true +HTTP 500 +[Asserts] +body == "redirect-or-server: 500" + +# mixed-range handle_errors matches the range (3xx) +GET https://localhost:9443/redir +[Options] +insecure: true +HTTP 301 +[Asserts] +body == "redirect-or-server: 301" + +# unrelated range still matches its dedicated handler +GET https://localhost:9443/client +[Options] +insecure: true +HTTP 400 +[Asserts] +body == "client: 400" + + +# Configure Caddy with two specific-code handlers in declaration order +# Regression guard: the matcher-set sort must preserve user-declared order +# when neither block is a catch-all (otherwise a status-specific block can +# end up swapped with another and the wrong body is returned). +POST http://localhost:2019/load +Content-Type: text/caddyfile +``` +{ + skip_install_trust + http_port 9080 + https_port 9443 + local_certs +} +localhost { + error /forbidden "Forbidden" 403 + error /missing "Missing" 404 + handle_errors 403 { + respond "first: 403" + } + handle_errors 404 { + respond "second: 404" + } +} +``` + +# 403 hits the first handler +GET https://localhost:9443/forbidden +[Options] +insecure: true +HTTP 403 +[Asserts] +body == "first: 403" + +# 404 hits the second handler +GET https://localhost:9443/missing +[Options] +insecure: true +HTTP 404 +[Asserts] +body == "second: 404" + + +# Configure Caddy with specific handlers declared before a catch-all +# This pins the only intentional reordering rule: catch-all goes last. +POST http://localhost:2019/load +Content-Type: text/caddyfile +``` +{ + skip_install_trust + http_port 9080 + https_port 9443 + local_certs +} +localhost { + error /forbidden "Forbidden" 403 + error /teapot "Teapot" 418 + handle_errors 403 { + respond "specific: 403" + } + handle_errors { + respond "catch-all: {err.status_code}" + } +} +``` + +# specific handler still wins when declared before the catch-all +GET https://localhost:9443/forbidden +[Options] +insecure: true +HTTP 403 +[Asserts] +body == "specific: 403" + +# catch-all picks up codes the specific handler does not list +GET https://localhost:9443/teapot +[Options] +insecure: true +HTTP 418 +[Asserts] +body == "catch-all: 418" +