diff --git a/apisix/plugins/error-page.lua b/apisix/plugins/error-page.lua index 89c880bfbf4b..bbfed054986a 100644 --- a/apisix/plugins/error-page.lua +++ b/apisix/plugins/error-page.lua @@ -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 = { @@ -94,7 +101,7 @@ local function get_metadata(ctx) return nil end - if status < 404 then + if status < 403 then return nil end @@ -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 diff --git a/docs/en/latest/plugins/error-page.md b/docs/en/latest/plugins/error-page.md index 45786d78e6e8..0372c99cef59 100644 --- a/docs/en/latest/plugins/error-page.md +++ b/docs/en/latest/plugins/error-page.md @@ -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. | @@ -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": "

404 Not Found

Request ID: $request_id

", + "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`): diff --git a/docs/zh/latest/plugins/error-page.md b/docs/zh/latest/plugins/error-page.md index 3cef324824d0..d5f7ecada836 100644 --- a/docs/zh/latest/plugins/error-page.md +++ b/docs/zh/latest/plugins/error-page.md @@ -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 | 响应体的内容类型。 | @@ -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": "

404 页面未找到

请求 ID:$request_id

", + "content_type": "text/html" + } +} +``` + +变量可以写作 `$var` 或 `${var}`,并支持使用 `??` 运算符指定默认值(例如 `$http_x_custom_id??unknown`)。如需输出字面量美元符号,请使用反斜杠转义(例如 `\$100` 将原样输出 `\$100`)。 + + ## 启用插件 `error-page` 插件默认禁用。要启用该插件,请将其添加到配置文件(`conf/config.yaml`)中: diff --git a/t/plugin/error-page.t b/t/plugin/error-page.t index 65afefe6f58f..a12ca788129c 100644 --- a/t/plugin/error-page.t +++ b/t/plugin/error-page.t @@ -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); @@ -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": "rid=$request_id host=$host addr=${remote_addr}"}, + "error_500": {"body": "custom 500"}, + "error_502": {"body": "custom 502"}, + "error_503": {"body": "custom 503"} + }]] + ) + + 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": "missing=$nonexistent_var??fallback"} + }]] + ) + + 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": "price=\$100 rid=$request_id"} + }]] + ) + + 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": "

403 Forbidden

"} + }]] + ) + + 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/

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": "

403 custom

"} + }]] + ) + 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/