| title | Custom Error Pages |
|---|---|
| sidebar_position | 17 |
Render custom error responses with template-based HTML, JSON, and XML formats. Supports per-route configuration, status code mapping with fallback chains, and content negotiation via the Accept header.
error_pages:
enabled: true
pages:
"404":
json: '{"error":"not found","code":{{.StatusCode}}}'
html: '<h1>{{.StatusCode}} — Not Found</h1><p>The requested path {{.RequestPath}} was not found.</p>'
"5xx":
json: '{"error":"server error","code":{{.StatusCode}}}'
"default":
json: '{"error":"{{.StatusText}}","code":{{.StatusCode}}}'
html: '<h1>Error {{.StatusCode}}</h1><p>{{.StatusText}}</p>'routes:
- id: my-api
path: /api
backends:
- url: http://backend:8080
error_pages:
enabled: true
pages:
"404":
json: '{"error":"resource not found","request_id":"{{.RequestID}}"}'
"429":
json: '{"error":"rate limited","retry_after":60}'Per-route keys override global keys. Unmatched global keys are inherited.
error_pages:
enabled: true
pages:
"default":
html_file: /etc/runway/error.html
json_file: /etc/runway/error.jsonInline and file are mutually exclusive per format (html vs html_file, etc.). File contents are read and compiled at startup.
Page keys determine which status codes are matched:
| Key | Matches |
|---|---|
"404" |
Exactly status 404 |
"4xx" |
Any status 400-499 |
"5xx" |
Any status 500-599 |
"default" |
Any error status (>= 400) |
When an error status code occurs, the lookup order is:
- Exact match — e.g.,
"404"for status 404 - Class match — e.g.,
"4xx"for any 4xx status - Default —
"default"key - Pass through — if no match, the original response is sent unmodified
The response format is chosen from the client's Accept header:
| Accept Header | Format |
|---|---|
text/html |
HTML |
application/json |
JSON |
application/xml, text/xml |
XML |
*/* or unrecognized |
JSON (default) |
If the negotiated format has no template for the matched page, the best available format is used instead.
All templates (HTML, JSON, XML) are Go text/template templates with access to these variables:
| Variable | Description |
|---|---|
{{.StatusCode}} |
HTTP status code (e.g., 404) |
{{.StatusText}} |
HTTP status text (e.g., "Not Found") |
{{.ErrorMessage}} |
Error message if available |
{{.RequestID}} |
X-Request-ID value |
{{.RequestMethod}} |
HTTP method (GET, POST, etc.) |
{{.RequestPath}} |
Request URL path |
{{.Host}} |
Request Host header |
{{.Timestamp}} |
Current time in RFC3339 format |
{{.RouteID}} |
Gateway route ID |
Note: Error page templates use standard Go
text/templateonly. Sprig functions are not available in error page templates.
{"error":"{{.StatusText}}","code":{{.StatusCode}},"path":"{{.RequestPath}}","request_id":"{{.RequestID}}","timestamp":"{{.Timestamp}}"}
<!DOCTYPE html>
<html>
<head><title>Error {{.StatusCode}}</title></head>
<body>
<h1>{{.StatusCode}} — {{.StatusText}}</h1>
<p>Request ID: {{.RequestID}}</p>
</body>
</html>Error pages are applied at middleware step 4.1, after variable context setup (step 4) and before access logging (step 4.25). This means error pages intercept errors from:
- Rate limiting (429)
- Authentication (401, 403)
- WAF (403)
- Circuit breaker (503)
- Throttle/priority (503)
- Validation (400)
- Proxy errors (502, 504)
Errors from IP filtering (step 2) and CORS (step 3) are not intercepted, as they occur before the error pages middleware.
curl http://localhost:8081/error-pagesReturns per-route configuration and render metrics:
{
"my-api": {
"pages": ["404", "5xx", "default"],
"metrics": {
"total_rendered": 42
}
}
}- Page keys must be exact status codes (100-599), class patterns (
1xx-5xx), or"default" - Inline and file are mutually exclusive per format (e.g.,
html+html_fileis an error) - Each page entry must define at least one format (html, json, or xml)
- Inline templates must be valid Go
text/templatesyntax - File paths must exist at config load time