mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
fix(openresty): preserve origin error status on custom error pages
Remove error_page '=' form that adopted the internal URI status (often 200) and left ngx.status as 0. Resolve the original code from $status/upstream and set ngx.status before rendering the HTML body.
This commit is contained in:
@@ -29,6 +29,7 @@ sidebar: false
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复源站错误页在边缘返回 HTTP 200、页面状态码显示异常(如 0)的问题:错误响应现在正确透传上游状态码,并在页面中展示真实状态码。
|
||||
- 修复 Agent 在配置已对齐但磁盘校验和不一致时,Pages 等对账成功后仍保留 `LastError` 的问题,避免偶发网络失败被健康事件长期显示为「活动中」且无法自动恢复。
|
||||
|
||||
### 改进
|
||||
|
||||
@@ -135,6 +135,11 @@ func renderOriginErrorPageIntercept(cfg ConfigSnapshot) string {
|
||||
|
||||
// renderOriginErrorPageServerBits emits server-level error_page + internal location.
|
||||
// Returns empty string when disabled, expand fails, or no codes remain.
|
||||
//
|
||||
// IMPORTANT: do NOT use `error_page CODE = /uri` (equals without response code).
|
||||
// That form adopts the status returned by the error URI; content_by_lua defaults
|
||||
// to 200 and ngx.status is often 0, so clients saw 200 with body "{{status}}"→"0".
|
||||
// Without `=`, nginx keeps the original error status for the internal redirect.
|
||||
func renderOriginErrorPageServerBits(cfg ConfigSnapshot) string {
|
||||
if !cfg.OriginErrorPageEnabled {
|
||||
return ""
|
||||
@@ -148,31 +153,59 @@ func renderOriginErrorPageServerBits(cfg ConfigSnapshot) string {
|
||||
parts[i] = strconv.Itoa(code)
|
||||
}
|
||||
var builder strings.Builder
|
||||
fmt.Fprintf(&builder, " error_page %s = %s;\n", strings.Join(parts, " "), OriginErrorPageInternalLocation)
|
||||
// No `=` — preserve original error status (502 stays 502).
|
||||
fmt.Fprintf(&builder, " error_page %s %s;\n", strings.Join(parts, " "), OriginErrorPageInternalLocation)
|
||||
builder.WriteString(renderOriginErrorPageInternalLocation())
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func renderOriginErrorPageInternalLocation() 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
|
||||
// written as `%%` so Sprintf does not treat them as format verbs.
|
||||
return fmt.Sprintf(` location = %s {
|
||||
internal;
|
||||
default_type text/html;
|
||||
charset utf-8;
|
||||
content_by_lua_block {
|
||||
local function resolve_error_status()
|
||||
local code = tonumber(ngx.var.status)
|
||||
if code and code >= 400 then
|
||||
return code
|
||||
end
|
||||
local upstream = ngx.var.upstream_status or ""
|
||||
-- multi-upstream: "502, 502" or failed connect "0"
|
||||
local first = upstream:match("(%%d+)")
|
||||
code = tonumber(first)
|
||||
if code and code >= 400 then
|
||||
return code
|
||||
end
|
||||
code = tonumber(ngx.status)
|
||||
if code and code >= 400 then
|
||||
return code
|
||||
end
|
||||
return 502
|
||||
end
|
||||
|
||||
local code = resolve_error_status()
|
||||
ngx.status = code
|
||||
|
||||
local f = io.open("%s", "r")
|
||||
if not f then
|
||||
ngx.status = ngx.status
|
||||
ngx.say("Error ", ngx.status)
|
||||
ngx.header["Content-Type"] = "text/html; charset=utf-8"
|
||||
ngx.say("Error ", tostring(code))
|
||||
return
|
||||
end
|
||||
local body = f:read("*a")
|
||||
f:close()
|
||||
local status = tostring(ngx.status)
|
||||
local status = tostring(code)
|
||||
local host = ngx.var.host or ""
|
||||
body = body:gsub("{{status}}", status, 1)
|
||||
body = body:gsub("{{host}}", host, 1)
|
||||
body = body:gsub("{{status}}", status)
|
||||
body = body:gsub("{{host}}", host)
|
||||
-- function replacer: plain insert, no percent pattern side effects
|
||||
body = body:gsub("{{status}}", function() return status end)
|
||||
body = body:gsub("{{host}}", function() return host end)
|
||||
ngx.header["Content-Type"] = "text/html; charset=utf-8"
|
||||
ngx.say(body)
|
||||
}
|
||||
|
||||
@@ -30,8 +30,19 @@ func TestRenderOriginErrorPageEnabled(t *testing.T) {
|
||||
if !strings.Contains(out, "error_page 500") {
|
||||
t.Fatalf("expected expanded status codes in error_page, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "= /__openflare_origin_error") {
|
||||
t.Fatal("error_page must keep original status via = redirect form")
|
||||
// Must NOT use `error_page … = /uri` (adopts error-URI status → often 200).
|
||||
// `location = /path` is unrelated and expected.
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "error_page ") && strings.Contains(trimmed, " = ") {
|
||||
t.Fatalf("error_page must not use '=' form, got: %s", trimmed)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(out, "error_page ") || !strings.Contains(out, " /__openflare_origin_error;") {
|
||||
t.Fatal("error_page must redirect to internal location without '='")
|
||||
}
|
||||
if !strings.Contains(out, "resolve_error_status") || !strings.Contains(out, "ngx.status = code") {
|
||||
t.Fatal("internal location must resolve and set ngx.status to the original error code")
|
||||
}
|
||||
if !strings.Contains(out, ErrorPageTmplPlaceholder) {
|
||||
t.Fatal("missing error page template placeholder")
|
||||
@@ -151,8 +162,8 @@ func TestRenderOriginErrorPageCustomHTMLInSupportFile(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(out, "error_page 502 = /__openflare_origin_error") {
|
||||
t.Fatalf("expected single 502 error_page, got:\n%s", out)
|
||||
if !strings.Contains(out, "error_page 502 /__openflare_origin_error;") {
|
||||
t.Fatalf("expected single 502 error_page without '=', got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user