feat(openresty): add origin error page GET-only option

Allow restricting custom origin error HTML to GET requests so other
methods pass through origin responses. Adds option seed, snapshot field,
edge limit_except/Lua handling, and admin UI switch.
This commit is contained in:
ryan
2026-08-06 20:22:44 +08:00
parent d17d8457f3
commit 6738abdec1
13 changed files with 118 additions and 7 deletions
+28 -5
View File
@@ -130,7 +130,17 @@ func renderOriginErrorPageIntercept(cfg ConfigSnapshot) string {
if _, err := ExpandStatusCodeTags(effectiveOriginErrorPageStatusTags(cfg)); err != nil {
return ""
}
return " proxy_intercept_errors on;\n"
// Always enable intercept for GET (and all methods when get_only is false).
// limit_except GET applies to non-GET methods: turn intercept off so origin
// error bodies (e.g. JSON 5xx) pass through unchanged.
var builder strings.Builder
builder.WriteString(" proxy_intercept_errors on;\n")
if cfg.OriginErrorPageGetOnly {
builder.WriteString(" limit_except GET {\n")
builder.WriteString(" proxy_intercept_errors off;\n")
builder.WriteString(" }\n")
}
return builder.String()
}
// renderOriginErrorPageServerBits emits server-level error_page + internal location.
@@ -155,22 +165,30 @@ func renderOriginErrorPageServerBits(cfg ConfigSnapshot) string {
var builder strings.Builder
// No `=` — preserve original error status (502 stays 502).
fmt.Fprintf(&builder, " error_page %s %s;\n", strings.Join(parts, " "), OriginErrorPageInternalLocation)
builder.WriteString(renderOriginErrorPageInternalLocation())
builder.WriteString(renderOriginErrorPageInternalLocation(cfg.OriginErrorPageGetOnly))
return builder.String()
}
func renderOriginErrorPageInternalLocation() string {
func renderOriginErrorPageInternalLocation(getOnly bool) string {
// Resolve status from $status (set by error_page internal redirect), then
// upstream_status, then ngx.status. Force ngx.status so the client receives
// the real error code. Use function replacers so host/status with `%` are safe.
//
// Note: fmt.Sprintf is used only for the two path placeholders; Lua `%` must be
// Note: fmt.Sprintf is used only for the path placeholders; Lua `%` must be
// written as `%%` so Sprintf does not treat them as format verbs.
//
// When getOnly is true, non-GET that still hit this location (e.g. nginx-local
// 502 without upstream body) exit with the original status and no HTML body.
getOnlyLua := "false"
if getOnly {
getOnlyLua = "true"
}
return fmt.Sprintf(` location = %s {
internal;
default_type text/html;
charset utf-8;
content_by_lua_block {
local get_only = %s
local function resolve_error_status()
local code = tonumber(ngx.var.status)
if code and code >= 400 then
@@ -193,6 +211,11 @@ func renderOriginErrorPageInternalLocation() string {
local code = resolve_error_status()
ngx.status = code
if get_only and ngx.req.get_method() ~= "GET" then
-- Non-GET: do not replace with HTML; exit with status only.
return ngx.exit(code)
end
local f = io.open("%s", "r")
if not f then
ngx.header["Content-Type"] = "text/html; charset=utf-8"
@@ -210,5 +233,5 @@ func renderOriginErrorPageInternalLocation() string {
ngx.say(body)
}
}
`, OriginErrorPageInternalLocation, ErrorPageTmplPlaceholder)
`, OriginErrorPageInternalLocation, getOnlyLua, ErrorPageTmplPlaceholder)
}
@@ -68,6 +68,40 @@ func TestRenderOriginErrorPageEnabled(t *testing.T) {
}
}
func TestRenderOriginErrorPageGetOnly(t *testing.T) {
t.Parallel()
doc := Document{
Routes: []Route{{
ID: 1, SiteName: "ex", Domains: []string{"ex.test"},
OriginURL: "http://127.0.0.1:9", Enabled: true,
}},
OpenRestyConfig: ConfigSnapshot{
OriginErrorPageEnabled: true,
OriginErrorPageStatusCodes: []string{"500-599"},
OriginErrorPageGetOnly: true,
},
}
out, err := RenderRouteConfig(doc, nil)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "proxy_intercept_errors on") {
t.Fatal("missing intercept on")
}
if !strings.Contains(out, "limit_except GET") {
t.Fatal("get_only must emit limit_except GET")
}
if !strings.Contains(out, "proxy_intercept_errors off") {
t.Fatal("get_only must turn intercept off for non-GET")
}
if !strings.Contains(out, `get_only = true`) {
t.Fatal("internal location must set get_only = true")
}
if !strings.Contains(out, `ngx.req.get_method() ~= "GET"`) {
t.Fatal("internal location must skip HTML for non-GET")
}
}
func TestRenderOriginErrorPageDisabled(t *testing.T) {
t.Parallel()
doc := Document{
+2
View File
@@ -318,6 +318,8 @@ type ConfigSnapshot struct {
OriginErrorPageEnabled bool `json:"origin_error_page_enabled"`
OriginErrorPageStatusCodes []string `json:"origin_error_page_status_codes,omitempty"`
OriginErrorPageHTML string `json:"origin_error_page_html,omitempty"`
// OriginErrorPageGetOnly limits custom error HTML to GET requests; other methods pass through.
OriginErrorPageGetOnly bool `json:"origin_error_page_get_only,omitempty"`
}
// Document is the top-level input structure for the OpenResty renderer,