@@ -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