Origin Error Page Design
You will learn: when an origin or the gateway returns a specified error status code, how OpenFlare replaces the pass-through response with a globally configurable page; how the config enters the immutable config version; and how the edge OpenResty keeps the real HTTP status code while displaying it in the page.
This design is the productized complement of the reverse proxy traffic path in System Architecture; the config release model is in Agent & Publish Model.
1. Goals and Non-Goals
1.1 Goals
- Interceptable: for a user-configured status code set, replace the previously pass-through origin/Nginx default error response with a unified HTML.
- Disableable: when the global switch is off, behavior matches today (pass-through / Nginx default page).
- Visible by default: enabled by default, default status code tag
500-599, default minimal OpenFlare error page. - Customizable: admins can edit the full HTML online; empty HTML means the built-in default template.
- Status passthrough: the HTTP response
statuskeeps the original error code (e.g. 502, 522); the page body shows the same value via. - Globally unified: a single config under sidebar「Website Management → Response Pages」shared by all reverse proxy routes.
- Consistent with release: the config persists via Option, enters the config version snapshot, and is distributed with release/rollback.
1.2 Non-Goals
- Per-route / per-Zone error page overrides
- Hosting error pages via file upload (online HTML only)
- Modifying WAF / PoW / rate-limit's own response pages (unless the user adds those status codes to the list)
- Pages static route error pages
- Multi-language error pages, brand asset CDN
2. Product Behavior
2.1 When to Replace
| Condition | Behavior |
|---|---|
| Switch on and the response status falls in the expanded set | return custom/default HTML, status unchanged |
| Switch on with GET-only enabled, non-GET request returns a matching status | pass through the origin's raw response, no replacement |
| Switch off | no error_page directives generated, pass through |
| Status not in the set | no replacement |
| Pages upstream routes | this feature is not applied |
| Origin returns 2xx/3xx/4xx successfully (not configured) | no replacement |
In all-methods mode, proxy_intercept_errors on is enabled on the reverse proxy location, so origin-returned matching 5xx etc. are also intercepted, not just gateway-local 502s; GET-only mode switches to Lua header/body filters that only replace GET response bodies.
2.2 Status Code Tag Syntax
Each Tags Input entry:
| Form | Example | Meaning |
|---|---|---|
| Single code | 522 | only that code |
| Closed range | 500-599 | expand including endpoints |
- Valid range: single codes and range endpoints must be in 400–599;
lo ≤ hi. - Default tag list:
["500-599"]. - Persist the raw tags (JSON array string); expand, dedupe, and sort at render time.
- If the expanded result is empty while enabled → save rejected.
- Invalid tags → save rejected with a readable error.
2.3 Page Placeholders
| Placeholder | Meaning |
|---|---|
| the current response status code (consistent with the HTTP status) |
| request Host |
Both custom HTML and the default template support these placeholders; replaced at the edge at runtime. Unused placeholders may be omitted from the template.
2.4 Default Page
Built-in minimal white-background OpenFlare default page: large pass-through status code, short English description, Host, and a brand footer. Supports / ; the frontend can load prebuilt styles from the built-in template catalog on the edit page.
3. Config Model
3.1 Option Keys (w_system_configs / OpenFlare Option API)
| Key | Type | Default | Description |
|---|---|---|---|
origin_error_page_enabled | bool string | true | master switch |
origin_error_page_status_codes | JSON string array | ["500-599"] | raw tags |
origin_error_page_html | text | "" | empty = built-in default; max 256 KiB |
origin_error_page_get_only | bool string | false | replace error pages only for GET; other methods pass through |
Reuses APIs:
GET /api/v1/d/optionPOST /api/v1/d/option/update-batch
No new resource routes. goose migration writes the seed; constants defined in the internal/model config key area.
3.2 Validation (update-batch)
enabled: parseable as bool.status_codes: valid JSON array; each entry^\d{3}$or^\d{3}-\d{3}$; expanded values all in 400–599; non-empty when enabled.html: length ≤ 256 KiB (bytes); empty allowed.- Parse/expand logic is a pure function shared by the API and
pkg/render/openrestyto avoid semantic forks.
No XSS sanitization on HTML: it's an admin global ops config consistent with public edge display; docs warn not to embed untrusted third-party scripts.
3.3 Config Version Snapshot
ConfigSnapshot adds fields:
OriginErrorPageEnabled bool
OriginErrorPageStatusCodes []string // raw tags
OriginErrorPageHTML string // empty => renderer uses built-in default
OriginErrorPageGetOnly boolRead from Option when building the snapshot; the Agent only consumes the snapshot, never reading the control-plane DB directly.
4. Edge Rendering
4.1 Content Generated When Enabled
- SupportFile: error page template (e.g.
error_pages/origin_error.html.tmpl), content is the custom HTML or built-in default, keeping/. - Each reverse proxy server (HTTP/HTTPS proxy; excluding Pages):
proxy_intercept_errors on;
error_page <expanded codes...> @__openflare_origin_error;
location @__openflare_origin_error {
default_type text/html;
charset utf-8;
content_by_lua_block {
# read template, replace {{status}} / {{host}}, output body
# ngx.status keeps the original error code
}
}4.2 Runtime Replacement
Use a named location with content_by_lua_block to read the template and replace placeholders — the status is not baked into a static file (status differs per request). GET-only mode uses header_filter_by_lua_block + body_filter_by_lua_block inside the reverse proxy location to replace only GET response bodies; non-GET requests pass through.
Never rewrite the error page to HTTP 200.
4.3 When Disabled
Do not output proxy_intercept_errors, error_page, the internal location, or the corresponding SupportFile (or the file may be written but unreferenced). GET-only mode also omits the Lua filters.
4.4 Interaction with Cache / Stale
If global proxy_cache_use_stale returns stale cache for some error codes, successful stale responses never enter error_page. The error page is only shown when the client actually receives an error status in the configured list. Behavior depends on existing cache directives; this feature does not change stale policy.
5. Frontend
5.1 Entry
- Sidebar「Website Management → Response Pages」: Error Page tab (
/responses), edit page/responses/error-page/edit, preview page/responses/error-page/preview.
5.2 Page Structure
- Header note: takes effect after releasing via「Version Release」.
- Switch + Tags Input (shadcn-extension Tags Input:
@/components/ui/tags-input): status code tags. - HTML editor area +「Load default template」「Restore default (clear)」+ placeholder docs.
- Client-side preview: replace with sample
status=502,host=example.comand preview in sandbox/iframe. - Save:
OptionService.updateBatch; permissions same as the performance tuning page (admin).
5.3 Component Dependencies
Tags Input and the HTML editor reuse existing shadcn/ui components, consistent with the existing UI style.
6. Data Flow
Admin /responses (Error Page tab)
→ Option update-batch (validate tags & HTML)
→ w_system_configs
Release config version
→ snapshot writes OriginErrorPage*
→ render OpenResty conf + SupportFile
→ Agent pulls and reloads
Visitor requests a proxied domain
→ origin/gateway produces a matching status code
→ error_page → named location
→ replace placeholders, keep original status, return HTML7. Decision Record
| Decision | Choice | Reason |
|---|---|---|
| Config scope | global | product requirement; simple implementation and ops |
| Storage | Option + config version | consistent with performance tuning, rollbackable |
| Status input | tags: single code and range | default whole 5xx, but can name 522 |
| Response status | keep original | correct for monitoring/SEO/client semantics |
| Runtime replacement | internal + lightweight template replacement | status differs per request |
| Customization | online HTML | flexible without a file-upload chain |