diff --git a/docs/changelog/index.md b/docs/changelog/index.md index d5eb411a..b73aa1d1 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -29,6 +29,7 @@ sidebar: false ### 修复 +- 修复源站错误页在边缘返回 HTTP 200、页面状态码显示异常(如 0)的问题:错误响应现在正确透传上游状态码,并在页面中展示真实状态码。 - 修复 Agent 在配置已对齐但磁盘校验和不一致时,Pages 等对账成功后仍保留 `LastError` 的问题,避免偶发网络失败被健康事件长期显示为「活动中」且无法自动恢复。 ### 改进 diff --git a/pkg/render/openresty/origin_error_page.go b/pkg/render/openresty/origin_error_page.go index 49075d08..874b5c7f 100644 --- a/pkg/render/openresty/origin_error_page.go +++ b/pkg/render/openresty/origin_error_page.go @@ -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) } diff --git a/pkg/render/openresty/origin_error_page_test.go b/pkg/render/openresty/origin_error_page_test.go index 9e21bc18..2ce4eca7 100644 --- a/pkg/render/openresty/origin_error_page_test.go +++ b/pkg/render/openresty/origin_error_page_test.go @@ -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) } }