Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions apisix/plugins/error-page.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ local metadata_schema = {
type = "object",
properties = {
enable = {type = "boolean", default = false},
error_403 = {
type = "object",
properties = {
body = {type = "string", default = err_body("403 Forbidden")},
content_type = {type = "string", default = "text/html"},
}
},
error_404 = {
type = "object",
properties = {
Expand Down Expand Up @@ -94,7 +101,7 @@ local function get_metadata(ctx)
return nil
end

if status < 404 then
if status < 403 then
return nil
end

Expand Down Expand Up @@ -126,17 +133,27 @@ function _M.header_filter(conf, ctx)
end
local status = ngx.status
local err_page = ctx.plugin_error_page_meta["error_" .. status]

-- resolve Nginx variables (e.g. $request_id, $remote_addr) in the body,
-- then set content-length based on the final rendered content
local body, err = core.utils.resolve_var(err_page.body, ctx.var)
if not body then
core.log.warn("failed to resolve variables in error-page body: ", err)
body = err_page.body
end
ctx.plugin_error_page_body = body

core.response.set_header("content-type", err_page.content_type)
core.response.set_header("content-length", #err_page.body)
core.response.set_header("content-length", #body)
end


function _M.body_filter(conf, ctx)
if not ctx.plugin_error_page_meta then
if not ctx.plugin_error_page_body then
return
end

ngx.arg[1] = ctx.plugin_error_page_meta["error_" .. ngx.status].body
ngx.arg[1] = ctx.plugin_error_page_body
ngx.arg[2] = true
end

Expand Down
18 changes: 18 additions & 0 deletions docs/en/latest/plugins/error-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ There are no attributes to configure this Plugin on Routes, Services, or other r
| Name | Type | Required | Default | Description |
| ---- | ---- | -------- | ------- | ----------- |
| enable | boolean | False | false | Set to `true` to enable the Plugin. |
| error_403 | object | False | | Custom error page for 403 responses. |
| error_403.body | string | False | Default HTML page | Response body for 403 responses. |
| error_403.content_type | string | False | text/html | Content type of the response body. |
| error_404 | object | False | | Custom error page for 404 responses. |
| error_404.body | string | False | Default HTML page | Response body for 404 responses. |
| error_404.content_type | string | False | text/html | Content type of the response body. |
Expand All @@ -58,6 +61,21 @@ There are no attributes to configure this Plugin on Routes, Services, or other r
| error_503.body | string | False | Default HTML page | Response body for 503 responses. |
| error_503.content_type | string | False | text/html | Content type of the response body. |

The `body` of each error page supports Nginx variables, which are resolved before the response is sent. For example, you can embed the request ID to help users and administrators correlate error responses with logs:

```json
{
"enable": true,
"error_404": {
"body": "<html><body><h1>404 Not Found</h1><p>Request ID: $request_id</p></body></html>",
"content_type": "text/html"
}
}
```

Variables can be written as `$var` or `${var}`, and a default value can be specified with the `??` operator (e.g. `$http_x_custom_id??unknown`). To output a literal dollar sign, escape it with a backslash (e.g. `\$100` renders as `\$100`).


## Enable Plugin

The `error-page` Plugin is disabled by default. To enable the Plugin, add it to your configuration file (`conf/config.yaml`):
Expand Down
18 changes: 18 additions & 0 deletions docs/zh/latest/plugins/error-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ description: error-page 插件允许自定义 APISIX 生成的 HTTP 错误响应
| 名称 | 类型 | 必选项 | 默认值 | 描述 |
| ---- | ---- | ------ | ------ | ---- |
| enable | boolean | 否 | false | 设为 `true` 以启用插件。 |
| error_403 | object | 否 | | 403 响应的自定义错误页面配置。 |
| error_403.body | string | 否 | 默认 HTML 页面 | 403 响应的响应体内容。 |
| error_403.content_type | string | 否 | text/html | 响应体的内容类型。 |
| error_404 | object | 否 | | 404 响应的自定义错误页面配置。 |
| error_404.body | string | 否 | 默认 HTML 页面 | 404 响应的响应体内容。 |
| error_404.content_type | string | 否 | text/html | 响应体的内容类型。 |
Expand All @@ -58,6 +61,21 @@ description: error-page 插件允许自定义 APISIX 生成的 HTTP 错误响应
| error_503.body | string | 否 | 默认 HTML 页面 | 503 响应的响应体内容。 |
| error_503.content_type | string | 否 | text/html | 响应体的内容类型。 |

每个错误页面的 `body` 支持 Nginx 变量,变量会在响应发送前解析。例如可以嵌入请求 ID,方便用户和管理员将错误响应与日志关联:

```json
{
"enable": true,
"error_404": {
"body": "<html><body><h1>404 页面未找到</h1><p>请求 ID:$request_id</p></body></html>",
"content_type": "text/html"
}
}
```

变量可以写作 `$var` 或 `${var}`,并支持使用 `??` 运算符指定默认值(例如 `$http_x_custom_id??unknown`)。如需输出字面量美元符号,请使用反斜杠转义(例如 `\$100` 将原样输出 `\$100`)。


## 启用插件

`error-page` 插件默认禁用。要启用该插件,请将其添加到配置文件(`conf/config.yaml`)中:
Expand Down
199 changes: 199 additions & 0 deletions t/plugin/error-page.t
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ add_block_preprocessor(sub {
my $user_yaml_config = <<_EOC_;
plugins:
- error-page
- ip-restriction
- serverless-post-function
_EOC_
$block->set_value("extra_yaml_config", $user_yaml_config);
Expand Down Expand Up @@ -425,3 +426,201 @@ GET /nginx-error-test
qr/502 custom/
--- error_log
connect() failed (111: Connection refused)


=== TEST 21: set plugin metadata with Nginx variables in error page body
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/error-page',
ngx.HTTP_PUT,
[[{
"enable": true,
"error_404": {"body": "<html><body>rid=$request_id host=$host addr=${remote_addr}</body></html>"},
"error_500": {"body": "<html><body>custom 500</body></html>"},
"error_502": {"body": "<html><body>custom 502</body></html>"},
"error_503": {"body": "<html><body>custom 503</body></html>"}
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed



=== TEST 22: Nginx variables in error page body are resolved
--- request
GET /hello
--- more_headers
X-Test-Status: 404
--- error_code: 404
--- response_headers
content-type: text/html
--- response_body_like eval
qr/rid=[0-9a-f]{32} host=localhost addr=127\.0\.0\.1/



=== TEST 23: variable with default value operator
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/error-page',
ngx.HTTP_PUT,
[[{
"enable": true,
"error_404": {"body": "<html><body>missing=$nonexistent_var??fallback</body></html>"}
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed



=== TEST 24: undefined variable falls back to default value
--- request
GET /hello
--- more_headers
X-Test-Status: 404
--- error_code: 404
--- response_body_like eval
qr/missing=fallback/



=== TEST 25: escaped dollar sign is not resolved
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/error-page',
ngx.HTTP_PUT,
[[{
"enable": true,
"error_404": {"body": "<html><body>price=\$100 rid=$request_id</body></html>"}
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed



=== TEST 26: escaped dollar sign stays literal, real variable resolved
--- request
GET /hello
--- more_headers
X-Test-Status: 404
--- error_code: 404
--- response_body_like eval
qr/price=\\\$100 rid=[0-9a-f]{32}/



=== TEST 27: set plugin metadata with custom error page for 403
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/error-page',
ngx.HTTP_PUT,
[[{
"enable": true,
"error_403": {"body": "<html><body><h1>403 Forbidden</h1></body></html>"}
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed



=== TEST 28: custom error page for 403
--- request
GET /hello
--- more_headers
X-Test-Status: 403
--- error_code: 403
--- response_headers
content-type: text/html
--- response_body_like eval
qr/<body><h1>403 Forbidden<\/h1><\/body>/



=== TEST 29: set metadata and create route with ip-restriction
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/error-page',
ngx.HTTP_PUT,
[[{
"enable": true,
"error_403": {"body": "<html><body><h1>403 custom</h1></body></html>"}
}]]
)
if code >= 300 then
ngx.status = code
ngx.say(body)
return
end
local code2, body2 = t('/apisix/admin/routes/4',
ngx.HTTP_PUT,
[[{
"uri": "/ip-restriction-test",
"plugins": {
"ip-restriction": {
"whitelist": ["192.0.2.0/24"]
}
},
"upstream": {
"nodes": {"127.0.0.1:1980": 1},
"type": "roundrobin"
}
}]]
)
if code2 >= 300 then
ngx.status = code2
end
ngx.say(body2)
}
}
--- response_body
passed



=== TEST 30: 403 response from ip-restriction is intercepted by error-page plugin
--- request
GET /ip-restriction-test
--- error_code: 403
--- response_body_like eval
qr/403 custom/
Loading