@@ -36,6 +36,7 @@ import (
3636 networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
3737 networkingv1alpha1 "go.datum.net/network-services-operator/api/v1alpha1"
3838 "go.datum.net/network-services-operator/internal/config"
39+ extassets "go.datum.net/network-services-operator/internal/extensionserver/assets"
3940 extcache "go.datum.net/network-services-operator/internal/extensionserver/cache"
4041 extmetrics "go.datum.net/network-services-operator/internal/extensionserver/metrics"
4142 "go.datum.net/network-services-operator/internal/extensionserver/mutate"
@@ -110,6 +111,44 @@ func NewCommand() *cobra.Command {
110111 return cmd
111112}
112113
114+ // buildLocalReplyConfig assembles the branded error-page configuration for the
115+ // extension server from the operator's ErrorPageConfig.
116+ //
117+ // Content sourcing is fail-safe by design: it starts from the page compiled
118+ // into the operator image and, only if ErrorPage.BodyPath points at a readable
119+ // non-empty file, swaps in that override. A missing, unreadable, or empty
120+ // override logs a warning and keeps the embedded default — it NEVER fails
121+ // startup. This guarantees the downstream EG extension hook (failOpen:false)
122+ // always has a valid body to inject and is never stalled by a content problem.
123+ //
124+ // Injection itself is gated on ErrorPage.Enabled via the Disabled flag.
125+ func buildLocalReplyConfig (cfg config.ErrorPageConfig , log * slog.Logger ) mutate.LocalReplyConfig {
126+ body := extassets .DefaultError5xxHTML
127+
128+ if cfg .BodyPath != "" {
129+ data , err := os .ReadFile (cfg .BodyPath )
130+ switch {
131+ case err != nil :
132+ log .Warn ("read error-page body override; using embedded default" ,
133+ "path" , cfg .BodyPath , "err" , err )
134+ case len (data ) == 0 :
135+ log .Warn ("error-page body override is empty; using embedded default" ,
136+ "path" , cfg .BodyPath )
137+ default :
138+ body = string (data )
139+ log .Info ("using error-page body override" , "path" , cfg .BodyPath , "bytes" , len (data ))
140+ }
141+ }
142+
143+ return mutate.LocalReplyConfig {
144+ Disabled : ! cfg .Enabled ,
145+ MinStatusCode : cfg .MinStatusCode ,
146+ RuntimeKey : cfg .RuntimeKey ,
147+ BodyHTML : body ,
148+ ContentType : cfg .ContentType ,
149+ }
150+ }
151+
113152// run starts the extension server with the given options. It owns cache startup,
114153// gRPC server lifecycle, and signal handling.
115154func run (o options ) {
@@ -205,6 +244,7 @@ func run(o options) {
205244 },
206245 ConnectorInternalListener : serverConfig .Gateway .ConnectorTunnelListenerName (),
207246 CorazaRouteBaseDirectives : coraza .RouteBaseDirectives ,
247+ LocalReply : buildLocalReplyConfig (serverConfig .Gateway .ErrorPage , log ),
208248 }
209249
210250 // --- gRPC panic recovery interceptor ---
0 commit comments