Skip to content

Commit 2c25ccb

Browse files
committed
Add built-in HEALTHCHECK_PATH setting
Requests to this path return a 200 response before host validation, HTTPS redirect, or any other middleware runs. This avoids common issues with load balancers and PaaS health checkers that use internal hostnames or plain HTTP.
1 parent bcd8913 commit 2c25ccb

File tree

4 files changed

+47
-0
lines changed

4 files changed

+47
-0
lines changed

plain/plain/http/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [Default response headers](#default-response-headers)
1616
- [Content Security Policy (CSP)](#content-security-policy-csp)
1717
- [Middleware](#middleware)
18+
- [Healthcheck](#healthcheck)
1819
- [Exceptions](#exceptions)
1920
- [FAQs](#faqs)
2021
- [Installation](#installation)
@@ -309,6 +310,22 @@ class TimingMiddleware(HttpMiddleware):
309310
return response
310311
```
311312

313+
## Healthcheck
314+
315+
The `HEALTHCHECK_PATH` setting provides a built-in healthcheck endpoint for load balancers, Kubernetes probes, and PaaS platforms like Railway.
316+
317+
```python
318+
# app/settings.py
319+
HEALTHCHECK_PATH = "/up/"
320+
```
321+
322+
When set, requests to this exact path return a `200` response before any other middleware runs — bypassing host validation, HTTPS redirects, and authentication. This avoids two common issues with health checkers:
323+
324+
1. **ALLOWED_HOSTS rejection** — the health checker uses an internal hostname not in the allowlist
325+
2. **HTTPS redirect loops** — the health checker sends plain HTTP without proxy headers
326+
327+
By default, `HEALTHCHECK_PATH` is empty (disabled).
328+
312329
## Exceptions
313330

314331
Raise exceptions to return specific HTTP error responses.

plain/plain/internal/handlers/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
# These middleware classes are always used by Plain.
2424
BUILTIN_BEFORE_MIDDLEWARE = [
25+
"plain.internal.middleware.healthcheck.HealthcheckMiddleware", # Respond to healthcheck before anything else
2526
"plain.internal.middleware.hosts.HostValidationMiddleware", # Validate Host header first
2627
"plain.internal.middleware.headers.DefaultHeadersMiddleware", # Runs after response, to set missing headers
2728
"plain.internal.middleware.https.HttpsRedirectMiddleware", # Runs before response, to redirect to HTTPS quickly
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from plain.http import HttpMiddleware, Response
6+
from plain.runtime import settings
7+
8+
if TYPE_CHECKING:
9+
from collections.abc import Callable
10+
11+
from plain.http import Request
12+
13+
14+
class HealthcheckMiddleware(HttpMiddleware):
15+
def __init__(self, get_response: Callable[[Request], Response]):
16+
super().__init__(get_response)
17+
self.healthcheck_path = settings.HEALTHCHECK_PATH
18+
19+
def process_request(self, request: Request) -> Response:
20+
if self.healthcheck_path and request.path_info == self.healthcheck_path:
21+
return Response("ok", content_type="text/plain", status_code=200)
22+
23+
return self.get_response(request)

plain/plain/runtime/global_settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
# - "192.168.1.0/24" matches IP addresses in that CIDR range
3434
ALLOWED_HOSTS: list[str] = []
3535

36+
# Path for the built-in healthcheck endpoint.
37+
# When set, requests to this exact path return a 200 "ok" response
38+
# before host validation, HTTPS redirect, or any other middleware runs.
39+
# Example: HEALTHCHECK_PATH = "/up/"
40+
HEALTHCHECK_PATH: str = ""
41+
3642
# Default headers for all responses.
3743
# Header values can include {request.attribute} placeholders for dynamic content.
3844
# Example: "script-src 'nonce-{request.csp_nonce}'" will use the request's nonce.

0 commit comments

Comments
 (0)