feat(openresty): render origin error page directives

Wire origin error page into OpenResty proxy route rendering: ConfigSnapshot
fields, default HTML template SupportFile, proxy_intercept_errors + error_page
with status-preserving internal Lua location, and Agent placeholder substitution
for __OPENFLARE_ERROR_PAGE_TMPL__. Pages routes are excluded.
This commit is contained in:
ryan
2026-08-06 13:52:40 +08:00
parent 07e835c543
commit 68d8f786cc
6 changed files with 418 additions and 44 deletions
+5
View File
@@ -522,6 +522,9 @@ func (m *Manager) CurrentChecksum() (string, error) {
}
normalizedRoute := string(data)
if m.NginxCertDir != "" {
// Longer error-page path must be restored before the cert-dir prefix rewrite.
errorPagePath := filepath.ToSlash(filepath.Join(m.NginxCertDir, openrestyrender.OriginErrorPageSupportPath))
normalizedRoute = strings.ReplaceAll(normalizedRoute, errorPagePath, openrestyrender.ErrorPageTmplPlaceholder)
normalizedRoute = strings.ReplaceAll(normalizedRoute, m.NginxCertDir, openrestyrender.CertDirPlaceholder)
}
if luaDir := m.luaRuntimePath(); luaDir != "" {
@@ -1375,6 +1378,8 @@ func (m *Manager) renderRouteConfig(content string) string {
rendered := content
if m.NginxCertDir != "" {
rendered = strings.ReplaceAll(rendered, openrestyrender.CertDirPlaceholder, m.NginxCertDir)
errorPagePath := filepath.ToSlash(filepath.Join(m.NginxCertDir, openrestyrender.OriginErrorPageSupportPath))
rendered = strings.ReplaceAll(rendered, openrestyrender.ErrorPageTmplPlaceholder, errorPagePath)
}
if luaDir := m.luaRuntimePath(); luaDir != "" {
rendered = strings.ReplaceAll(rendered, openrestyrender.LuaDirPlaceholder, luaDir)
+6 -1
View File
@@ -15,6 +15,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
sharedprotocol "github.com/Rain-kl/Wavelet/pkg/protocol"
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
)
type runCall struct {
@@ -299,13 +300,17 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) {
if !strings.Contains(string(routeData), "/etc/nginx/openflare-certs/1.crt") {
t.Fatalf("expected placeholder replacement in route config, got %s", string(routeData))
}
renderedRoute := manager.renderRouteConfig("access_by_lua_file __OPENFLARE_LUA_DIR__/pow/check.lua;\nlocation /.within.website/x/cmd/anubis/static/ { alias __OPENFLARE_POW_STATIC_DIR__/; }\n")
renderedRoute := manager.renderRouteConfig("access_by_lua_file __OPENFLARE_LUA_DIR__/pow/check.lua;\nlocation /.within.website/x/cmd/anubis/static/ { alias __OPENFLARE_POW_STATIC_DIR__/; }\nio.open(\"__OPENFLARE_ERROR_PAGE_TMPL__\", \"r\")\n")
if !strings.Contains(renderedRoute, "access_by_lua_file /etc/nginx/openflare-lua/pow/check.lua;") {
t.Fatalf("expected lua dir placeholder replacement in route config, got %s", renderedRoute)
}
if !strings.Contains(renderedRoute, "alias /etc/nginx/openflare-lua/pow/static/;") {
t.Fatalf("expected pow static dir placeholder replacement in route config, got %s", renderedRoute)
}
wantErrorPagePath := filepath.ToSlash(filepath.Join(manager.NginxCertDir, openrestyrender.OriginErrorPageSupportPath))
if !strings.Contains(renderedRoute, `io.open("`+wantErrorPagePath+`", "r")`) {
t.Fatalf("expected error page template placeholder replacement, got %s", renderedRoute)
}
mainData, err := os.ReadFile(manager.MainConfigPath)
if err != nil {
t.Fatalf("failed to read main config: %v", err)
+172
View File
@@ -0,0 +1,172 @@
package openresty
import (
"fmt"
"strconv"
"strings"
)
const (
// OriginErrorPageSupportPath is the SupportFile path for the origin error HTML template.
OriginErrorPageSupportPath = "error_pages/origin_error.html.tmpl"
// OriginErrorPageInternalLocation is the internal nginx location that serves the error body.
OriginErrorPageInternalLocation = "/__openflare_origin_error"
defaultOriginErrorPageStatusTag = "500-599"
)
// DefaultOriginErrorPageHTML is the built-in Cloudflare-style origin error page.
// Placeholders {{status}} and {{host}} are substituted at request time by Lua.
const DefaultOriginErrorPageHTML = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{status}} | 源站暂时无法提供服务</title>
<style>
:root { color-scheme: light dark; }
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
background: #f6f7f9;
color: #1f2937;
}
@media (prefers-color-scheme: dark) {
body { background: #0f1419; color: #e5e7eb; }
.card { background: #1a2332; border-color: #2d3a4d; box-shadow: none; }
.status { color: #f3f4f6; }
.muted { color: #9ca3af; }
.host { background: #243044; color: #d1d5db; }
}
.card {
width: min(32rem, calc(100% - 2rem));
padding: 2.5rem 2rem;
border-radius: 12px;
background: #fff;
border: 1px solid #e5e7eb;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
text-align: center;
}
.status {
margin: 0;
font-size: clamp(3.5rem, 12vw, 5.5rem);
font-weight: 700;
letter-spacing: -0.04em;
line-height: 1;
color: #111827;
}
h1 {
margin: 1rem 0 0.5rem;
font-size: 1.25rem;
font-weight: 600;
}
p { margin: 0.4rem 0; line-height: 1.6; }
.muted { color: #6b7280; font-size: 0.95rem; }
.host {
display: inline-block;
margin-top: 1.25rem;
padding: 0.35rem 0.75rem;
border-radius: 999px;
background: #f3f4f6;
color: #374151;
font-size: 0.85rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
word-break: break-all;
}
</style>
</head>
<body>
<main class="card" role="main">
<p class="status" aria-label="HTTP status">{{status}}</p>
<h1>源站暂时无法提供服务</h1>
<p class="muted">网关已拦截源站错误响应。请稍后重试;若问题持续,请联系站点管理员。</p>
<p class="host">{{host}}</p>
</main>
</body>
</html>
`
// EffectiveOriginErrorPageHTML returns custom HTML when set, otherwise the built-in default.
func EffectiveOriginErrorPageHTML(cfg ConfigSnapshot) string {
if strings.TrimSpace(cfg.OriginErrorPageHTML) == "" {
return DefaultOriginErrorPageHTML
}
return cfg.OriginErrorPageHTML
}
func effectiveOriginErrorPageStatusTags(cfg ConfigSnapshot) []string {
if len(cfg.OriginErrorPageStatusCodes) == 0 {
return []string{defaultOriginErrorPageStatusTag}
}
return cfg.OriginErrorPageStatusCodes
}
func originErrorPageSupportFile(cfg ConfigSnapshot) SupportFile {
return SupportFile{
Path: OriginErrorPageSupportPath,
Content: EffectiveOriginErrorPageHTML(cfg),
}
}
func renderOriginErrorPageIntercept(cfg ConfigSnapshot) string {
if !cfg.OriginErrorPageEnabled {
return ""
}
if _, err := ExpandStatusCodeTags(effectiveOriginErrorPageStatusTags(cfg)); err != nil {
return ""
}
return " proxy_intercept_errors on;\n"
}
// renderOriginErrorPageServerBits emits server-level error_page + internal location.
// Returns empty string when disabled, expand fails, or no codes remain.
func renderOriginErrorPageServerBits(cfg ConfigSnapshot) string {
if !cfg.OriginErrorPageEnabled {
return ""
}
codes, err := ExpandStatusCodeTags(effectiveOriginErrorPageStatusTags(cfg))
if err != nil || len(codes) == 0 {
return ""
}
parts := make([]string, len(codes))
for i, code := range codes {
parts[i] = strconv.Itoa(code)
}
var builder strings.Builder
fmt.Fprintf(&builder, " error_page %s = %s;\n", strings.Join(parts, " "), OriginErrorPageInternalLocation)
builder.WriteString(renderOriginErrorPageInternalLocation())
return builder.String()
}
func renderOriginErrorPageInternalLocation() string {
return fmt.Sprintf(` location = %s {
internal;
default_type text/html;
charset utf-8;
content_by_lua_block {
local f = io.open("%s", "r")
if not f then
ngx.status = ngx.status
ngx.say("Error ", ngx.status)
return
end
local body = f:read("*a")
f:close()
local status = tostring(ngx.status)
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)
ngx.header["Content-Type"] = "text/html; charset=utf-8"
ngx.say(body)
}
}
`, OriginErrorPageInternalLocation, ErrorPageTmplPlaceholder)
}
@@ -0,0 +1,185 @@
package openresty
import (
"strings"
"testing"
)
func TestRenderOriginErrorPageEnabled(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"},
},
}
out, err := RenderRouteConfig(doc, nil)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "proxy_intercept_errors on") {
t.Fatal("missing intercept")
}
if !strings.Contains(out, "error_page") || !strings.Contains(out, "/__openflare_origin_error") {
t.Fatal("missing error_page")
}
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")
}
if !strings.Contains(out, ErrorPageTmplPlaceholder) {
t.Fatal("missing error page template placeholder")
}
res, err := Render(doc, nil)
if err != nil {
t.Fatal(err)
}
found := false
for _, f := range res.SupportFiles {
if f.Path == OriginErrorPageSupportPath {
found = true
if !strings.Contains(f.Content, "{{status}}") {
t.Fatal("template missing placeholder")
}
if !strings.Contains(f.Content, "{{host}}") {
t.Fatal("template missing host placeholder")
}
}
}
if !found {
t.Fatal("missing support file")
}
}
func TestRenderOriginErrorPageDisabled(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: false},
}
out, err := RenderRouteConfig(doc, nil)
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, "proxy_intercept_errors") {
t.Fatal("should not intercept when disabled")
}
if strings.Contains(out, "/__openflare_origin_error") {
t.Fatal("should not emit internal error location when disabled")
}
res, err := Render(doc, nil)
if err != nil {
t.Fatal(err)
}
for _, f := range res.SupportFiles {
if f.Path == OriginErrorPageSupportPath {
t.Fatal("should not emit support file when disabled")
}
}
}
func TestRenderOriginErrorPageDefaultsEmptyHTMLAndStatusCodes(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,
},
}
out, err := RenderRouteConfig(doc, nil)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "error_page 500") {
t.Fatalf("empty status codes should default to 500-599, got:\n%s", out)
}
html := EffectiveOriginErrorPageHTML(doc.OpenRestyConfig)
if html != DefaultOriginErrorPageHTML {
t.Fatal("empty HTML should use default template")
}
if !strings.Contains(html, "{{status}}") || !strings.Contains(html, "{{host}}") {
t.Fatal("default HTML must include placeholders")
}
if !strings.Contains(html, "源站暂时无法提供服务") {
t.Fatal("default HTML missing neutral copy")
}
}
func TestRenderOriginErrorPageCustomHTMLInSupportFile(t *testing.T) {
t.Parallel()
custom := "<html><body>custom {{status}} @ {{host}}</body></html>"
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{"502"},
OriginErrorPageHTML: custom,
},
}
res, err := Render(doc, nil)
if err != nil {
t.Fatal(err)
}
found := false
for _, f := range res.SupportFiles {
if f.Path == OriginErrorPageSupportPath {
found = true
if f.Content != custom {
t.Fatalf("support file content = %q, want custom HTML", f.Content)
}
}
}
if !found {
t.Fatal("missing support file")
}
out, err := RenderRouteConfig(doc, nil)
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)
}
}
func TestRenderOriginErrorPageSkipsPagesRoutes(t *testing.T) {
t.Parallel()
doc := Document{
Routes: []Route{{
ID: 1, SiteName: "pages", Domains: []string{"pages.test"},
UpstreamType: "pages", Enabled: true,
PagesDeployment: &PagesDeployment{
ProjectID: 1, LocalRoot: PagesDirPlaceholder + "/projects/1/current",
EntryFile: "index.html",
},
}},
OpenRestyConfig: ConfigSnapshot{
OriginErrorPageEnabled: true,
OriginErrorPageStatusCodes: []string{"500-599"},
},
}
out, err := RenderRouteConfig(doc, nil)
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, "proxy_intercept_errors") {
t.Fatal("pages routes must not get proxy_intercept_errors")
}
if strings.Contains(out, "/__openflare_origin_error") {
t.Fatal("pages routes must not get origin error location")
}
}
+5 -2
View File
@@ -46,6 +46,9 @@ func Render(doc Document, certificateFiles []SupportFile) (*Result, error) {
}
files := append([]SupportFile(nil), certificateFiles...)
files = append(files, SupportFile{Path: "waf_config.json", Content: wafConfig})
if doc.OpenRestyConfig.OriginErrorPageEnabled {
files = append(files, originErrorPageSupportFile(doc.OpenRestyConfig))
}
files = DedupeSupportFiles(files)
return &Result{
MainConfig: mainConfig,
@@ -270,7 +273,7 @@ func renderOpenRestyObservabilityTemplateBlock() string {
}
func renderHTTPProxyServer(serverNames string, siteName string, originURL string, originHost string, customHeaders []CustomHeader, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, cfg ConfigSnapshot) string {
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s%s location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled))
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s%s location / {\n%s%s%s%s%s%s }\n%s%s}\n\n", serverNames, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderOriginErrorPageIntercept(cfg), renderProxyPassBlock(originURL, upstreamConfig), renderOriginErrorPageServerBits(cfg), renderPowStaticLocationBlock(powEnabled))
}
func renderPagesAPIProxyLocationBlock(deployment *PagesDeployment) string {
@@ -333,7 +336,7 @@ func renderHTTPSServer(serverNames string, siteName string, originURL string, or
h3Listen = " listen 443 quic;\n"
h3Header = " add_header Alt-Svc 'h3=\":443\"; ma=86400';\n"
}
return fmt.Sprintf("server {\n listen 443 ssl;\n%s http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s%s location / {\n%s%s%s%s%s }\n%s}\n\n", h3Listen, serverNames, certPath, keyPath, h3Header, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled))
return fmt.Sprintf("server {\n listen 443 ssl;\n%s http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s%s location / {\n%s%s%s%s%s%s }\n%s%s}\n\n", h3Listen, serverNames, certPath, keyPath, h3Header, renderAccessBlock(siteName, powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig, cfg), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderOriginErrorPageIntercept(cfg), renderProxyPassBlock(originURL, upstreamConfig), renderOriginErrorPageServerBits(cfg), renderPowStaticLocationBlock(powEnabled))
}
func renderHTTPSPagesServer(serverNames string, siteName string, certificateID uint, deployment *PagesDeployment, limitConfig routeLimitConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, cfg ConfigSnapshot) string {
+45 -41
View File
@@ -20,6 +20,7 @@ const (
ObservabilityPortPlaceholder = "__OPENFLARE_OBSERVABILITY_PORT__"
PowStaticDirPlaceholder = "__OPENFLARE_POW_STATIC_DIR__"
PagesDirPlaceholder = "__OPENFLARE_PAGES_DIR__"
ErrorPageTmplPlaceholder = "__OPENFLARE_ERROR_PAGE_TMPL__"
SourceConfigFileName = "openresty_config.json"
)
@@ -273,47 +274,50 @@ type WAFDocument struct {
// ConfigSnapshot holds the full set of OpenResty tuning parameters that are
// rendered into the nginx main configuration template.
type ConfigSnapshot struct {
DefaultServerReturnStatus int `json:"default_server_return_status"`
WorkerProcesses string `json:"worker_processes"`
WorkerConnections int `json:"worker_connections"`
WorkerRlimitNofile int `json:"worker_rlimit_nofile"`
EventsUse string `json:"events_use,omitempty"`
EventsMultiAcceptEnabled bool `json:"events_multi_accept_enabled"`
KeepaliveTimeout int `json:"keepalive_timeout"`
KeepaliveRequests int `json:"keepalive_requests"`
ClientHeaderTimeout int `json:"client_header_timeout"`
ClientBodyTimeout int `json:"client_body_timeout"`
ClientMaxBodySize string `json:"client_max_body_size"`
LargeClientHeaderBuffers string `json:"large_client_header_buffers"`
SendTimeout int `json:"send_timeout"`
ProxyConnectTimeout int `json:"proxy_connect_timeout"`
ProxySendTimeout int `json:"proxy_send_timeout"`
ProxyReadTimeout int `json:"proxy_read_timeout"`
WebsocketEnabled bool `json:"websocket_enabled"`
HTTP3Enabled bool `json:"http3_enabled"`
ProxyRequestBuffering bool `json:"proxy_request_buffering"`
ProxyBufferingEnabled bool `json:"proxy_buffering_enabled"`
ProxyBuffers string `json:"proxy_buffers"`
ProxyBufferSize string `json:"proxy_buffer_size"`
ProxyBusyBuffersSize string `json:"proxy_busy_buffers_size"`
GzipEnabled bool `json:"gzip_enabled"`
GzipMinLength int `json:"gzip_min_length"`
GzipCompLevel int `json:"gzip_comp_level"`
Resolvers string `json:"resolvers,omitempty"`
CacheEnabled bool `json:"cache_enabled"`
CachePath string `json:"cache_path,omitempty"`
CacheLevels string `json:"cache_levels"`
CacheInactive string `json:"cache_inactive"`
CacheMaxSize string `json:"cache_max_size"`
CacheKeyTemplate string `json:"cache_key_template"`
CacheLockEnabled bool `json:"cache_lock_enabled"`
CacheLockTimeout string `json:"cache_lock_timeout"`
CacheUseStale string `json:"cache_use_stale"`
MainConfigTemplate string `json:"main_config_template,omitempty"`
DefaultLimitConnPerServer int `json:"default_limit_conn_per_server,omitempty"`
DefaultLimitConnPerIP int `json:"default_limit_conn_per_ip,omitempty"`
DefaultLimitRate string `json:"default_limit_rate,omitempty"`
DefaultLimitReqPerIP string `json:"default_limit_req_per_ip,omitempty"`
DefaultServerReturnStatus int `json:"default_server_return_status"`
WorkerProcesses string `json:"worker_processes"`
WorkerConnections int `json:"worker_connections"`
WorkerRlimitNofile int `json:"worker_rlimit_nofile"`
EventsUse string `json:"events_use,omitempty"`
EventsMultiAcceptEnabled bool `json:"events_multi_accept_enabled"`
KeepaliveTimeout int `json:"keepalive_timeout"`
KeepaliveRequests int `json:"keepalive_requests"`
ClientHeaderTimeout int `json:"client_header_timeout"`
ClientBodyTimeout int `json:"client_body_timeout"`
ClientMaxBodySize string `json:"client_max_body_size"`
LargeClientHeaderBuffers string `json:"large_client_header_buffers"`
SendTimeout int `json:"send_timeout"`
ProxyConnectTimeout int `json:"proxy_connect_timeout"`
ProxySendTimeout int `json:"proxy_send_timeout"`
ProxyReadTimeout int `json:"proxy_read_timeout"`
WebsocketEnabled bool `json:"websocket_enabled"`
HTTP3Enabled bool `json:"http3_enabled"`
ProxyRequestBuffering bool `json:"proxy_request_buffering"`
ProxyBufferingEnabled bool `json:"proxy_buffering_enabled"`
ProxyBuffers string `json:"proxy_buffers"`
ProxyBufferSize string `json:"proxy_buffer_size"`
ProxyBusyBuffersSize string `json:"proxy_busy_buffers_size"`
GzipEnabled bool `json:"gzip_enabled"`
GzipMinLength int `json:"gzip_min_length"`
GzipCompLevel int `json:"gzip_comp_level"`
Resolvers string `json:"resolvers,omitempty"`
CacheEnabled bool `json:"cache_enabled"`
CachePath string `json:"cache_path,omitempty"`
CacheLevels string `json:"cache_levels"`
CacheInactive string `json:"cache_inactive"`
CacheMaxSize string `json:"cache_max_size"`
CacheKeyTemplate string `json:"cache_key_template"`
CacheLockEnabled bool `json:"cache_lock_enabled"`
CacheLockTimeout string `json:"cache_lock_timeout"`
CacheUseStale string `json:"cache_use_stale"`
MainConfigTemplate string `json:"main_config_template,omitempty"`
DefaultLimitConnPerServer int `json:"default_limit_conn_per_server,omitempty"`
DefaultLimitConnPerIP int `json:"default_limit_conn_per_ip,omitempty"`
DefaultLimitRate string `json:"default_limit_rate,omitempty"`
DefaultLimitReqPerIP string `json:"default_limit_req_per_ip,omitempty"`
OriginErrorPageEnabled bool `json:"origin_error_page_enabled"`
OriginErrorPageStatusCodes []string `json:"origin_error_page_status_codes,omitempty"`
OriginErrorPageHTML string `json:"origin_error_page_html,omitempty"`
}
// Document is the top-level input structure for the OpenResty renderer,