diff --git a/internal/apps/agent/nginx/manager.go b/internal/apps/agent/nginx/manager.go
index 57bc5a34..f0824802 100644
--- a/internal/apps/agent/nginx/manager.go
+++ b/internal/apps/agent/nginx/manager.go
@@ -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)
diff --git a/internal/apps/agent/nginx/manager_test.go b/internal/apps/agent/nginx/manager_test.go
index 51db7f50..c96dc8e4 100644
--- a/internal/apps/agent/nginx/manager_test.go
+++ b/internal/apps/agent/nginx/manager_test.go
@@ -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)
diff --git a/pkg/render/openresty/origin_error_page.go b/pkg/render/openresty/origin_error_page.go
new file mode 100644
index 00000000..a09ff13b
--- /dev/null
+++ b/pkg/render/openresty/origin_error_page.go
@@ -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 = `
+
+
+
+
+{{status}} | 源站暂时无法提供服务
+
+
+
+
+ {{status}}
+ 源站暂时无法提供服务
+ 网关已拦截源站错误响应。请稍后重试;若问题持续,请联系站点管理员。
+ {{host}}
+
+
+
+`
+
+// 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)
+}
diff --git a/pkg/render/openresty/origin_error_page_test.go b/pkg/render/openresty/origin_error_page_test.go
new file mode 100644
index 00000000..165b13b2
--- /dev/null
+++ b/pkg/render/openresty/origin_error_page_test.go
@@ -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 := "custom {{status}} @ {{host}}"
+ 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")
+ }
+}
diff --git a/pkg/render/openresty/render.go b/pkg/render/openresty/render.go
index 569bea5b..3676621a 100644
--- a/pkg/render/openresty/render.go
+++ b/pkg/render/openresty/render.go
@@ -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 {
diff --git a/pkg/render/openresty/types.go b/pkg/render/openresty/types.go
index fec553d4..66124ada 100644
--- a/pkg/render/openresty/types.go
+++ b/pkg/render/openresty/types.go
@@ -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,