How Caddy Error Handling Works: HandlerError and the handle_errors Directive
Caddy error handling operates through a structured HandlerError type that carries HTTP status codes, unique IDs, and stack traces, combined with the handle_errors directive that registers sub-routes executed when specific error status codes are encountered.
The caddyserver/caddy repository implements a robust error handling mechanism that allows both programmatic error generation in Go handlers and declarative error response configuration via the Caddyfile. This system ensures that HTTP errors are logged, optionally processed through custom handler chains, and returned to clients with appropriate status codes and traceability.
Core Components of Caddy Error Handling
Caddy's error handling architecture centers on three interconnected components that work together to capture, route, and respond to errors.
HandlerError Structured Error Type
The HandlerError struct defined in modules/caddyhttp/errors.go serves as the foundation for all HTTP error handling. Unlike standard Go errors, HandlerError encapsulates:
- StatusCode: The HTTP status code (e.g., 404, 500)
- ID: A unique identifier for the error instance
- Trace: A stack trace capturing where the error originated
- Err: The underlying wrapped error
Handlers use the caddyhttp.Error helper function to create these structured errors. When invoked, this function automatically generates a random ID and captures the current stack trace, providing observability that standard errors cannot offer.
The handle_errors Directive
The handle_errors Caddyfile directive, parsed by the parseHandleErrors function in caddyconfig/httpcaddyfile/builtins.go (lines 837-909), defines sub-routes that execute only when errors match specific status codes.
When Caddy parses this directive, it constructs a matcher expression using the format {http.error.status_code} in [404,410] and registers the enclosed block as an "error route." These routes are compiled into s.errorHandlerChain during server provisioning in modules/caddyhttp/app.go.
Server Error Flow Orchestration
The Server.ServeHTTP method in modules/caddyhttp/server.go (lines 990-1080) orchestrates the complete error handling workflow. This method manages the transition from primary handler execution to error processing, including request state restoration, logging, and fallback response generation.
The Caddy Error Handling Execution Flow
Understanding the precise sequence of operations helps developers intercept and process errors effectively.
1. Error Generation in Handlers
When a handler encounters a failure condition, it returns a structured error using the helper function:
return caddyhttp.Error(http.StatusNotFound, fmt.Errorf("page not found"))
This creates a HandlerError with a unique ID and stack trace, signaling to the server that an HTTP error response is required.
2. Primary Handler Chain Completion
After the compiled primary handler chain executes in Server.serveHTTP, any non-nil error triggers the error handling pathway. The server checks if the returned error is a HandlerError to extract specific status codes and metadata.
3. Request State Restoration
Before invoking error handlers, Caddy restores the original request state. Lines 996-1004 in server.go reset the request's method, URL, and remote address to their initial values, ensuring that error handlers work with unmutated request objects even if middleware earlier in the chain modified them.
4. Error Context Injection
The server stores the error in the request context using ErrorCtxKey (defined in modules/caddyhttp/errors.go). This makes the error available to downstream modules, including templates that can access error data via {{ .httpError }}.
5. Structured Logging
The errLogValues function extracts the status code, error message, and structured fields from the HandlerError, writing them to the configured logger before any response is sent to the client.
6. Error Route Execution
If the server configuration includes handle_errors blocks, Caddy executes the compiled s.errorHandlerChain.ServeHTTP. The matcher expression ensures only errors with matching status codes trigger specific handlers.
If the error route succeeds (returns nil), Caddy considers the error handled and logs it without generating a default error response.
7. Fallback Response Generation
When no error route exists or the route itself fails, Caddy falls back to default behavior (lines 48-52 in server.go):
- If the error is a
HandlerError, use itsStatusCode - Otherwise, return 500 Internal Server Error
The server writes the final status code header and completes the request.
Practical Implementation Examples
Configuring Custom Error Pages in Caddyfile
The handle_errors block defines sub-routes for specific status codes:
example.com {
respond "Hello, world!"
}
# Custom error handling for 404 and 410
handle_errors 404 410 {
file_server {
root /var/www/errors
}
}
This configuration parses into a matcher expression {http.error.status_code} in [404,410] and executes the file_server handler only when those errors occur.
Returning Structured Errors in Go Handlers
Go modules can leverage Caddy's error handling by importing the caddyhttp package:
package main
import (
"fmt"
"net/http"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func myHandler(w http.ResponseWriter, r *http.Request) error {
if !authorized(r) {
return caddyhttp.Error(http.StatusForbidden,
fmt.Errorf("access denied for %s", r.RemoteAddr))
}
w.Write([]byte("OK"))
return nil
}
The caddyhttp.Error function populates the HandlerError.ID and HandlerError.Trace fields automatically, providing debugging information for logs and templates.
Accessing Error Data in Templates
Templates can render error-specific content by accessing the injected error context:
{{ if .httpError }}
<h1>Error {{ .httpError.StatusCode }}</h1>
<p>Trace ID: {{ .httpError.ID }}</p>
<pre>{{ .httpError.Trace }}</pre>
{{ end }}
This functionality is implemented in modules/caddyhttp/templates/tplcontext.go via the TemplateContext.funcHTTPError method, which retrieves the error from the request context using ErrorCtxKey.
Key Source Files and Functions
The following files in the caddyserver/caddy repository implement the error handling system:
modules/caddyhttp/errors.go: DefinesHandlerError, theErrorhelper function, andErrorCtxKeyfor context storage.caddyconfig/httpcaddyfile/builtins.go(lines 837-909): ContainsparseHandleErrors, which parses thehandle_errorsdirective and builds matcher expressions.modules/caddyhttp/server.go(lines 990-1080): ImplementsServeHTTPwith the core error handling flow, including request restoration (lines 996-1004) and fallback response logic (lines 48-52).modules/caddyhttp/app.go: Pre-compiles error handler chains during server provisioning.modules/caddyhttp/templates/tplcontext.go(lines 450-470): Exposes error data to templates through{{ .httpError }}.
Summary
- HandlerError provides structured error information including HTTP status codes, unique IDs, and stack traces, defined in
modules/caddyhttp/errors.go. - The
handle_errorsdirective creates matcher-driven sub-routes that execute when specific error status codes occur, parsed incaddyconfig/httpcaddyfile/builtins.go. - Caddy restores the original request state before executing error handlers to ensure consistent processing.
- Errors are injected into the request context via
ErrorCtxKey, making them accessible to templates and other modules. - The error handling flow prioritizes user-defined routes, falling back to default status codes only when routes fail or are undefined.
Frequently Asked Questions
How do I create a custom 404 page in Caddy?
Use the handle_errors directive in your Caddyfile to define a sub-route that serves static files when 404 errors occur. For example, handle_errors 404 { file_server { root /var/www/errors } } will serve content from /var/www/errors when a 404 status is triggered. The request state is automatically restored to its original condition before the error handler executes.
What information does Caddy include in structured errors?
Caddy's HandlerError struct includes the HTTP status code, a unique random ID for tracing, a complete stack trace captured at the error creation point, and the underlying error message. When using the caddyhttp.Error helper, the ID and trace are populated automatically, providing observability for debugging distributed requests.
Can I handle multiple HTTP error codes with a single handler?
Yes, the handle_errors directive accepts multiple status codes as arguments, such as handle_errors 404 410 500 { ... }. The Caddyfile parser builds a matcher expression using {http.error.status_code} in [404,410,500], causing the enclosed handler chain to execute for any of the specified status codes.
How can I access error details in Caddy templates?
Error details are available in templates through the {{ .httpError }} object, which exposes the StatusCode, ID, and Trace fields. This data is injected into the request context under ErrorCtxKey by the server error handling logic in modules/caddyhttp/server.go, allowing templates to render error-specific debugging information or custom error pages.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →