diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ef63a33d..c97e54965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ This changelog keeps track of work items that have been completed and are ready ### New -- **General**: TODO ([#TODO](https://github.com/kedacore/http-add-on/issues/TODO)) +- **Interceptor**: Add customizable cold start message for scale-to-zero applications ([#TBD](https://github.com/kedacore/http-add-on/pull/TBD)) ### Improvements diff --git a/config/crd/bases/http.keda.sh_httpscaledobjects.yaml b/config/crd/bases/http.keda.sh_httpscaledobjects.yaml index e7540b926..957322713 100644 --- a/config/crd/bases/http.keda.sh_httpscaledobjects.yaml +++ b/config/crd/bases/http.keda.sh_httpscaledobjects.yaml @@ -60,6 +60,12 @@ spec: spec: description: HTTPScaledObjectSpec defines the desired state of HTTPScaledObject properties: + coldStartMessage: + description: |- + (optional) Custom message to display when the application is scaled to zero. + If not set, a default message will be shown. + The message will be displayed in an auto-refreshing HTML page. + type: string coldStartTimeoutFailoverRef: description: (optional) The name of the failover service to route HTTP requests to when the target is not available diff --git a/interceptor/proxy_handlers.go b/interceptor/proxy_handlers.go index c9d885d58..931662613 100644 --- a/interceptor/proxy_handlers.go +++ b/interceptor/proxy_handlers.go @@ -90,6 +90,31 @@ func newForwardingHandler( conditionWaitTimeout = time.Duration(httpso.Spec.ColdStartTimeoutFailoverRef.TimeoutSeconds) * time.Second } + // NEW: Check if we should show cold start message immediately + // If ColdStartMessage is configured, check endpoints and return HTML immediately if scaled to zero + if httpso.Spec.ColdStartMessage != "" { + waitFuncCtx, done := context.WithTimeout(ctx, 100*time.Millisecond) + defer done() + isColdStart, err := waitFunc( + waitFuncCtx, + httpso.GetNamespace(), + httpso.Spec.ScaleTargetRef.Service, + ) + // If we're at zero replicas (cold start), immediately return warming page + if err != nil || isColdStart { + lggr.Info("Cold start detected, returning warming page", "namespace", httpso.GetNamespace(), "service", httpso.Spec.ScaleTargetRef.Service) + html := generateWarmingPageHTML(httpso.Spec.ColdStartMessage) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte(html)); err != nil { + lggr.Error(err, "failed to write warming page response") + } + return + } + // If we have endpoints, continue with normal flow + } + waitFuncCtx, done := context.WithTimeout(ctx, conditionWaitTimeout) defer done() isColdStart, err := waitFunc( diff --git a/interceptor/warming_page.go b/interceptor/warming_page.go new file mode 100644 index 000000000..c4bda86f4 --- /dev/null +++ b/interceptor/warming_page.go @@ -0,0 +1,96 @@ +package main + +import ( + "html" +) + +const defaultColdStartMessage = "Service is starting, please wait..." + +// generateWarmingPageHTML creates an HTML page to display during cold starts. +// The page includes: +// - Custom message (or default if empty) +// - Auto-refresh every 5 seconds +// - Loading animation +// - Professional styling +func generateWarmingPageHTML(customMessage string) string { + message := customMessage + if message == "" { + message = defaultColdStartMessage + } + + // Escape HTML to prevent XSS + safeMessage := html.EscapeString(message) + + return ` + +
+ + + +This page will automatically refresh...
+Powered by KEDA HTTP Add-on
+