Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c600fb6
feat: Add custom placeholder pages for scale-from-zero scenarios
malpou May 30, 2025
3edd3de
remove ClearCache funtion
malpou Jun 2, 2025
c2a6c46
use same image for e2e tests
malpou Jun 2, 2025
4f349c2
adjust tests according to comments
malpou Jun 2, 2025
8e7904a
add RWMutex to ensure that the template cache is locked when being wr…
malpou Jun 2, 2025
d15d207
adjust e2e tests away from my local setup and over to something that …
malpou Jun 2, 2025
6eb7272
Update placeholder_pages_test.go
malpou Jun 3, 2025
c94489c
take on injecting reloading watching js using HEAD requests
malpou Jun 3, 2025
d946985
remove outdated text
malpou Jun 3, 2025
4a7ea4e
fix unit tests
malpou Jun 3, 2025
5c36d7b
fix e2e tests
malpou Jun 3, 2025
67ed6dd
simplify e2e tests
malpou Jun 3, 2025
351e607
remove sleep
malpou Jun 3, 2025
0584407
refinement
malpou Jun 3, 2025
d471171
Update interceptor/handler/placeholder.go
malpou Jun 4, 2025
2326bb6
Update tests/checks/placeholder_pages/placeholder_pages_test.go
malpou Jun 4, 2025
05e8345
linting issues
malpou Jun 4, 2025
26c319d
Update interceptor/handler/placeholder.go
malpou Aug 16, 2025
7d8d97d
refactor: Address maintainer feedback on placeholder pages
malpou Aug 16, 2025
973f90d
pr feedback adjustments [not tested yet]
malpou Aug 20, 2025
eec6c8c
feat: make placeholder pages content-agnostic
malpou Oct 4, 2025
06322d2
fix: add defensive nil check for placeholderHandler
malpou Oct 4, 2025
d97f584
refactor: improve placeholder handler code quality
malpou Oct 4, 2025
995060f
Update kustomization.yaml
malpou Oct 4, 2025
e0469b7
Update kustomization.yaml
malpou Oct 4, 2025
f3b28a9
Update kustomization.yaml
malpou Oct 4, 2025
43ffaab
Update proxy_handlers.go
malpou Oct 4, 2025
f8f26ce
Update helper.go
malpou Oct 4, 2025
f77f07f
Update CHANGELOG.md
malpou Oct 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ This changelog keeps track of work items that have been completed and are ready
- **General**: Allow using HSO and SO with different names ([#1293](https://github.com/kedacore/http-add-on/issues/1293))
- **General**: Support profiling for KEDA components ([#4789](https://github.com/kedacore/keda/issues/4789))
- **General**: Add possibility to skip TLS verification for upstreams in interceptor ([#1307](https://github.com/kedacore/http-add-on/pull/1307))
- **General**: Add custom placeholder responses for scale-from-zero scenarios ([#874](https://github.com/kedacore/http-add-on/issues/874))
### Improvements

- **Interceptor**: Support HTTPScaledObject scoped timeout ([#813](https://github.com/kedacore/http-add-on/issues/813))
Expand Down
39 changes: 39 additions & 0 deletions config/crd/bases/http.keda.sh_httpscaledobjects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,45 @@ spec:
items:
type: string
type: array
placeholderConfig:
description: (optional) Configuration for placeholder pages during
scale-from-zero
properties:
content:
description: |-
Inline content for placeholder response. Supports any format (HTML, JSON, XML, plain text, etc.)
Content is processed as a Go template with variables: ServiceName, Namespace, RefreshInterval, RequestID, Timestamp
Content-Type should be set via the Headers field to match your content format
type: string
enabled:
default: false
description: Enable placeholder response when replicas are scaled
to zero
type: boolean
headers:
additionalProperties:
type: string
description: |-
Additional HTTP headers to include with placeholder response
Use this to set Content-Type matching your content format (e.g., 'Content-Type: application/json')
type: object
refreshInterval:
default: 5
description: |-
RefreshInterval is a template variable available in content (in seconds)
This is just data passed to the template, not used by the interceptor for automatic refresh
format: int32
maximum: 60
minimum: 1
type: integer
statusCode:
default: 503
description: HTTP status code to return with placeholder response
format: int32
maximum: 599
minimum: 100
type: integer
type: object
replicas:
description: (optional) Replica information
properties:
Expand Down
10 changes: 10 additions & 0 deletions config/interceptor/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ spec:
value: "10s"
- name: KEDA_HTTP_EXPECT_CONTINUE_TIMEOUT
value: "1s"
- name: KEDA_HTTP_PLACEHOLDER_DEFAULT_TEMPLATE_PATH
value: "/placeholder-templates/default.html"
ports:
- name: admin
containerPort: 9090
Expand Down Expand Up @@ -83,9 +85,17 @@ spec:
capabilities:
drop:
- ALL
volumeMounts:
- name: placeholder-templates
mountPath: /placeholder-templates
readOnly: true
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
serviceAccountName: interceptor
terminationGracePeriodSeconds: 10
volumes:
- name: placeholder-templates
configMap:
name: interceptor-placeholder-templates
3 changes: 2 additions & 1 deletion config/interceptor/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ resources:
- metrics.service.yaml
- service_account.yaml
- scaledobject.yaml
- placeholder-templates.yaml
configurations:
- transformerconfig.yaml
labels:
- includeSelectors: true
includeTemplates: true
pairs:
app.kubernetes.io/instance: interceptor
app.kubernetes.io/instance: interceptor
64 changes: 64 additions & 0 deletions config/interceptor/placeholder-templates.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: interceptor-placeholder-templates
data:
# Optional default template used when placeholderConfig.content is not specified
# This is an HTML example - users can create any format (JSON, XML, plain text, etc.)
# Users should always set the Content-Type header in their HTTPScaledObject to match their format
default.html: |
<!DOCTYPE html>
<html>
<head>
<title>Service Starting</title>
<meta charset="utf-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: #f5f5f5;
}
.container {
text-align: center;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 400px;
}
h1 {
color: #333;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.spinner {
width: 40px;
height: 40px;
margin: 1.5rem auto;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
p {
color: #666;
margin: 0.5rem 0;
}
</style>
</head>
<body>
<div class="container">
<h1>{{.ServiceName}} is starting up...</h1>
<div class="spinner"></div>
<p>Please wait while we prepare your service.</p>
</div>
</body>
</html>
99 changes: 99 additions & 0 deletions docs/ref/vX.X.X/http_scaled_object.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ spec:
window: 1m
concurrency:
targetValue: 100
placeholderConfig:
enabled: true
refreshInterval: 5
statusCode: 503
headers:
Content-Type: "text/html; charset=utf-8"
X-Service-Status: "warming-up"
content: |
<!DOCTYPE html>
<html>
<head>
<title>Service Starting</title>
<meta http-equiv="refresh" content="{{.RefreshInterval}}">
</head>
<body>
<h1>{{.ServiceName}} is starting...</h1>
</body>
</html>
```

This document is a narrated reference guide for the `HTTPScaledObject`.
Expand Down Expand Up @@ -134,3 +152,84 @@ This section enables scaling based on the request concurrency.
>Default: 100

This is the target value for the scaling configuration.

## `placeholderConfig`

This optional section enables serving placeholder responses when the workload is scaled to zero. When enabled, instead of returning an error while waiting for the workload to scale up, the interceptor will serve a customizable response with any content format.

### `enabled`

>Default: false

Whether to enable placeholder responses for this HTTPScaledObject.

### `refreshInterval`

>Default: 5

A template variable (in seconds) that can be used in your content template. This is just data passed to the template - it does not automatically refresh the response. You can use it in your content for client-side refresh logic if needed (e.g., `<meta http-equiv="refresh" content="{{.RefreshInterval}}">`).

### `statusCode`

>Default: 503

The HTTP status code to return with the placeholder response. Common values are 503 (Service Unavailable) or 202 (Accepted).

### `headers`

>Default: {}

A map of custom HTTP headers to include in the placeholder response. **Important**: Use this to set the `Content-Type` header to match your content format. For example:
- `Content-Type: text/html; charset=utf-8` for HTML
- `Content-Type: application/json` for JSON
- `Content-Type: text/plain` for plain text

### `content`

>Default: ConfigMap-provided template (if configured), otherwise returns simple text

Custom content for the placeholder response. Supports any format (HTML, JSON, XML, plain text, etc.). Content is processed as a Go template with the following variables:
- `{{.ServiceName}}` - The name of the service from scaleTargetRef
- `{{.Namespace}}` - The namespace of the HTTPScaledObject
- `{{.RefreshInterval}}` - The configured refresh interval value (just a number)
- `{{.RequestID}}` - The X-Request-ID header value if present
- `{{.Timestamp}}` - The current timestamp in RFC3339 format

**Examples:**

HTML with client-side refresh:
```yaml
content: |
<!DOCTYPE html>
<html>
<head>
<title>Service Starting</title>
<meta http-equiv="refresh" content="{{.RefreshInterval}}">
</head>
<body>
<h1>{{.ServiceName}} is starting...</h1>
</body>
</html>
headers:
Content-Type: "text/html; charset=utf-8"
```

JSON response:
```yaml
content: |
{
"status": "warming_up",
"service": "{{.ServiceName}}",
"namespace": "{{.Namespace}}",
"timestamp": "{{.Timestamp}}"
}
headers:
Content-Type: "application/json"
```

Plain text:
```yaml
content: "{{.ServiceName}} is starting up. Please retry in a few seconds."
headers:
Content-Type: "text/plain"
```
38 changes: 38 additions & 0 deletions examples/vX.X.X/httpscaledobject-json-api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Example: JSON placeholder for API-to-API communication
# Use case: Microservices calling other microservices
kind: HTTPScaledObject
apiVersion: http.keda.sh/v1alpha1
metadata:
name: api-service
spec:
hosts:
- api.example.com
scaleTargetRef:
name: api-backend
kind: Deployment
apiVersion: apps/v1
service: api-backend
port: 8080
replicas:
min: 0
max: 10
scalingMetric:
concurrency:
targetValue: 100
placeholderConfig:
enabled: true
refreshInterval: 10
statusCode: 202 # 202 Accepted - request received but not yet processed
headers:
Content-Type: "application/json"
Retry-After: "10"
X-Service-Status: "scaling"
content: |
{
"status": "warming_up",
"service": "{{.ServiceName}}",
"namespace": "{{.Namespace}}",
"retry_after_seconds": {{.RefreshInterval}},
"timestamp": "{{.Timestamp}}",
"message": "Service is scaling up to handle your request"
}
35 changes: 35 additions & 0 deletions examples/vX.X.X/httpscaledobject-plaintext.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Example: Plain text placeholder for simple clients
# Use case: CLI tools, simple HTTP clients, or monitoring systems
kind: HTTPScaledObject
apiVersion: http.keda.sh/v1alpha1
metadata:
name: simple-service
spec:
hosts:
- simple.example.com
scaleTargetRef:
name: simple-backend
kind: Deployment
apiVersion: apps/v1
service: simple-backend
port: 8080
replicas:
min: 0
max: 10
scalingMetric:
concurrency:
targetValue: 100
placeholderConfig:
enabled: true
refreshInterval: 3
statusCode: 503
headers:
Content-Type: "text/plain; charset=utf-8"
content: |
{{.ServiceName}} is currently unavailable.

The service is scaling up to handle your request.
Please retry in {{.RefreshInterval}} seconds.

Namespace: {{.Namespace}}
Timestamp: {{.Timestamp}}
36 changes: 36 additions & 0 deletions examples/vX.X.X/httpscaledobject-xml.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Example: XML placeholder for legacy systems
# Use case: SOAP services or XML-based APIs
kind: HTTPScaledObject
apiVersion: http.keda.sh/v1alpha1
metadata:
name: legacy-service
spec:
hosts:
- legacy.example.com
scaleTargetRef:
name: legacy-backend
kind: Deployment
apiVersion: apps/v1
service: legacy-backend
port: 8080
replicas:
min: 0
max: 5
scalingMetric:
concurrency:
targetValue: 50
placeholderConfig:
enabled: true
refreshInterval: 5
statusCode: 503
headers:
Content-Type: "application/xml; charset=utf-8"
content: |
<?xml version="1.0" encoding="UTF-8"?>
<response>
<status>unavailable</status>
<service>{{.ServiceName}}</service>
<namespace>{{.Namespace}}</namespace>
<message>Service is scaling up</message>
<retryAfter>{{.RefreshInterval}}</retryAfter>
</response>
Loading