@@ -6,15 +6,13 @@ import (
66 "fmt"
77 "html/template"
88 "net/http"
9+ "os"
910 "strings"
1011 "sync"
1112 "time"
1213
13- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14- "k8s.io/client-go/kubernetes"
15-
14+ "github.com/kedacore/http-add-on/interceptor/config"
1615 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1"
17- "github.com/kedacore/http-add-on/pkg/routing"
1816)
1917
2018const placeholderScript = `<script>
@@ -110,10 +108,11 @@ type cacheEntry struct {
110108
111109// PlaceholderHandler handles serving placeholder pages during scale-from-zero
112110type PlaceholderHandler struct {
113- k8sClient kubernetes.Interface
114- templateCache map [string ]* cacheEntry
115- cacheMutex sync.RWMutex
116- defaultTmpl * template.Template
111+ templateCache map [string ]* cacheEntry
112+ cacheMutex sync.RWMutex
113+ defaultTmpl * template.Template
114+ servingCfg * config.Serving
115+ enableScript bool
117116}
118117
119118// PlaceholderData contains data for rendering placeholder templates
@@ -126,19 +125,39 @@ type PlaceholderData struct {
126125}
127126
128127// NewPlaceholderHandler creates a new placeholder handler
129- func NewPlaceholderHandler (k8sClient kubernetes.Interface , routingTable routing.Table ) (* PlaceholderHandler , error ) {
130- // Combine the default template with the script
131- defaultTemplateWithScript := injectPlaceholderScript (defaultPlaceholderTemplateWithoutScript )
132- defaultTmpl , err := template .New ("default" ).Parse (defaultTemplateWithScript )
128+ func NewPlaceholderHandler (servingCfg * config.Serving ) (* PlaceholderHandler , error ) {
129+ var defaultTemplate string
130+
131+ // Try to load template from configured path
132+ if servingCfg .PlaceholderDefaultTemplatePath != "" {
133+ content , err := os .ReadFile (servingCfg .PlaceholderDefaultTemplatePath )
134+ if err == nil {
135+ defaultTemplate = string (content )
136+ } else {
137+ // Fall back to built-in template if file cannot be read
138+ fmt .Printf ("Warning: Could not read placeholder template from %s: %v. Using built-in template.\n " ,
139+ servingCfg .PlaceholderDefaultTemplatePath , err )
140+ defaultTemplate = defaultPlaceholderTemplateWithoutScript
141+ }
142+ } else {
143+ defaultTemplate = defaultPlaceholderTemplateWithoutScript
144+ }
145+
146+ // Inject script if enabled
147+ if servingCfg .PlaceholderEnableScript {
148+ defaultTemplate = injectPlaceholderScript (defaultTemplate )
149+ }
150+
151+ defaultTmpl , err := template .New ("default" ).Parse (defaultTemplate )
133152 if err != nil {
134153 return nil , fmt .Errorf ("failed to parse default template: %w" , err )
135154 }
136155
137156 return & PlaceholderHandler {
138- k8sClient : k8sClient ,
139- routingTable : routingTable ,
140157 templateCache : make (map [string ]* cacheEntry ),
141158 defaultTmpl : defaultTmpl ,
159+ servingCfg : servingCfg ,
160+ enableScript : servingCfg .PlaceholderEnableScript ,
142161 }, nil
143162}
144163
@@ -166,18 +185,45 @@ func injectPlaceholderScript(templateContent string) string {
166185 return templateContent + placeholderScript
167186 }
168187
169- // For non-HTML content, wrap it in a minimal HTML structure with the script
170- return fmt .Sprintf (`<!DOCTYPE html>
171- <html>
172- <head>
173- <meta charset="utf-8">
174- <title>Service Starting</title>
175- </head>
176- <body>
177- %s
178- %s
179- </body>
180- </html>` , templateContent , placeholderScript )
188+ // Don't wrap non-HTML content - return as-is
189+ return templateContent
190+ }
191+
192+ // detectContentType determines the appropriate content type based on Accept header and content
193+ func detectContentType (acceptHeader string , content string ) string {
194+ // Check Accept header for specific content types
195+ if strings .Contains (acceptHeader , "application/json" ) {
196+ return "application/json"
197+ }
198+ if strings .Contains (acceptHeader , "application/xml" ) {
199+ return "application/xml"
200+ }
201+ if strings .Contains (acceptHeader , "text/plain" ) {
202+ return "text/plain"
203+ }
204+
205+ // Default to HTML for browser requests or when HTML is accepted
206+ if strings .Contains (acceptHeader , "text/html" ) || strings .Contains (acceptHeader , "*/*" ) || acceptHeader == "" {
207+ // Check if content looks like HTML
208+ if strings .Contains (content , "<" ) && strings .Contains (content , ">" ) {
209+ return "text/html; charset=utf-8"
210+ }
211+ }
212+
213+ // Try to detect based on content
214+ trimmed := strings .TrimSpace (content )
215+ if (strings .HasPrefix (trimmed , "{" ) && strings .HasSuffix (trimmed , "}" )) ||
216+ (strings .HasPrefix (trimmed , "[" ) && strings .HasSuffix (trimmed , "]" )) {
217+ return "application/json"
218+ }
219+ if strings .HasPrefix (trimmed , "<" ) {
220+ if strings .HasPrefix (trimmed , "<?xml" ) {
221+ return "application/xml"
222+ }
223+ return "text/html; charset=utf-8"
224+ }
225+
226+ return "text/plain; charset=utf-8"
181227}
182228
183229// ServePlaceholder serves a placeholder page based on the HTTPScaledObject configuration
@@ -194,19 +240,19 @@ func (h *PlaceholderHandler) ServePlaceholder(w http.ResponseWriter, r *http.Req
194240 statusCode = http .StatusServiceUnavailable
195241 }
196242
243+ // Set custom headers first
197244 for k , v := range config .Headers {
198245 w .Header ().Set (k , v )
199246 }
200247
201- w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
202- w .Header ().Set ("X-KEDA-HTTP-Placeholder-Served" , "true" )
203- w .Header ().Set ("Cache-Control" , "no-cache, no-store, must-revalidate" )
204-
248+ // Get template and render content
205249 tmpl , err := h .getTemplate (r .Context (), hso )
206250 if err != nil {
251+ w .Header ().Set ("Content-Type" , "text/plain; charset=utf-8" )
252+ w .Header ().Set ("X-KEDA-HTTP-Placeholder-Served" , "true" )
253+ w .Header ().Set ("Cache-Control" , "no-cache, no-store, must-revalidate" )
207254 w .WriteHeader (statusCode )
208- fmt .Fprintf (w , "<h1>%s is starting up...</h1><meta http-equiv='refresh' content='%d'>" ,
209- hso .Spec .ScaleTargetRef .Service , config .RefreshInterval )
255+ fmt .Fprintf (w , "%s is starting up...\n " , hso .Spec .ScaleTargetRef .Service )
210256 return nil
211257 }
212258
@@ -220,14 +266,32 @@ func (h *PlaceholderHandler) ServePlaceholder(w http.ResponseWriter, r *http.Req
220266
221267 var buf bytes.Buffer
222268 if err := tmpl .Execute (& buf , data ); err != nil {
269+ w .Header ().Set ("Content-Type" , "text/plain; charset=utf-8" )
270+ w .Header ().Set ("X-KEDA-HTTP-Placeholder-Served" , "true" )
271+ w .Header ().Set ("Cache-Control" , "no-cache, no-store, must-revalidate" )
223272 w .WriteHeader (statusCode )
224- fmt .Fprintf (w , "<h1>%s is starting up...</h1><meta http-equiv='refresh' content='%d'>" ,
225- hso .Spec .ScaleTargetRef .Service , config .RefreshInterval )
273+ fmt .Fprintf (w , "%s is starting up...\n " , hso .Spec .ScaleTargetRef .Service )
226274 return nil
227275 }
228276
277+ content := buf .String ()
278+
279+ // Detect and set content type based on Accept header and content
280+ contentType := detectContentType (r .Header .Get ("Accept" ), content )
281+
282+ // For non-HTML content, don't inject script even if enabled
283+ isHTML := strings .Contains (contentType , "text/html" )
284+ if ! isHTML && h .enableScript && strings .Contains (content , placeholderScript ) {
285+ // Remove script from non-HTML content
286+ content = strings .ReplaceAll (content , placeholderScript , "" )
287+ }
288+
289+ w .Header ().Set ("Content-Type" , contentType )
290+ w .Header ().Set ("X-KEDA-HTTP-Placeholder-Served" , "true" )
291+ w .Header ().Set ("Cache-Control" , "no-cache, no-store, must-revalidate" )
292+
229293 w .WriteHeader (statusCode )
230- _ , err = w .Write (buf . Bytes ( ))
294+ _ , err = w .Write ([] byte ( content ))
231295 return err
232296}
233297
@@ -246,58 +310,20 @@ func (h *PlaceholderHandler) getTemplate(ctx context.Context, hso *v1alpha1.HTTP
246310 }
247311 h .cacheMutex .RUnlock ()
248312
249- injectedContent := injectPlaceholderScript (config .Content )
250- tmpl , err := template .New ("inline" ).Parse (injectedContent )
251- if err != nil {
252- return nil , err
253- }
254-
255313 h .cacheMutex .Lock ()
256- h .templateCache [cacheKey ] = & cacheEntry {
257- template : tmpl ,
258- hsoGeneration : hso .Generation ,
314+ content := config .Content
315+ // Only inject script for HTML-like content if enabled
316+ if h .enableScript && (strings .Contains (content , "<" ) && strings .Contains (content , ">" )) {
317+ content = injectPlaceholderScript (content )
259318 }
260- h .cacheMutex .Unlock ()
261- return tmpl , nil
262- }
263-
264- if config .ContentConfigMap != "" {
265- cacheKey := fmt .Sprintf ("%s/%s/cm/%s" , hso .Namespace , hso .Name , config .ContentConfigMap )
266-
267- cm , err := h .k8sClient .CoreV1 ().ConfigMaps (hso .Namespace ).Get (ctx , config .ContentConfigMap , metav1.GetOptions {})
268- if err != nil {
269- return nil , fmt .Errorf ("failed to get ConfigMap %s: %w" , config .ContentConfigMap , err )
270- }
271-
272- h .cacheMutex .RLock ()
273- entry , ok := h .templateCache [cacheKey ]
274- if ok && entry .hsoGeneration == hso .Generation && entry .configMapVersion == cm .ResourceVersion {
275- h .cacheMutex .RUnlock ()
276- return entry .template , nil
277- }
278- h .cacheMutex .RUnlock ()
279-
280- key := config .ContentConfigMapKey
281- if key == "" {
282- key = "template.html"
283- }
284-
285- content , ok := cm .Data [key ]
286- if ! ok {
287- return nil , fmt .Errorf ("key %s not found in ConfigMap %s" , key , config .ContentConfigMap )
288- }
289-
290- injectedContent := injectPlaceholderScript (content )
291- tmpl , err := template .New ("configmap" ).Parse (injectedContent )
319+ tmpl , err := template .New ("inline" ).Parse (content )
292320 if err != nil {
321+ h .cacheMutex .Unlock ()
293322 return nil , err
294323 }
295-
296- h .cacheMutex .Lock ()
297324 h .templateCache [cacheKey ] = & cacheEntry {
298- template : tmpl ,
299- hsoGeneration : hso .Generation ,
300- configMapVersion : cm .ResourceVersion ,
325+ template : tmpl ,
326+ hsoGeneration : hso .Generation ,
301327 }
302328 h .cacheMutex .Unlock ()
303329 return tmpl , nil
0 commit comments