Skip to content

Commit 05b04aa

Browse files
committed
Ref #519 -- Extend CLI HTTPS support and verbosity
* Enable default support for X-forwarded headers to handle HTTPS redirects. * Add more error handling to descritive outputs to support user debugging effors.
1 parent b01cffa commit 05b04aa

3 files changed

Lines changed: 143 additions & 21 deletions

File tree

docs/container.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,14 @@ urlpatterns = [
3434
… and then run the health check command:
3535

3636
```shell
37-
python manage.py health_check health_check-container localhost:8000
37+
python manage.py health_check health_check-container localhost:8000 --forwarded-host example.com
3838
```
3939

40+
> [!IMPORTANT]
41+
> When using the `health_check` command, ensure that the host is included in your `ALLOWED_HOSTS` setting.
42+
> The command automatically uses the first entry from `ALLOWED_HOSTS` for the `X-Forwarded-Host` header if available.
43+
> For SSL-enabled applications, use the `--forwarded-proto https` flag.
44+
4045
Your host name and port may vary depending on your container setup.
4146

4247
## Configuration Examples
@@ -80,13 +85,18 @@ spec:
8085
- name: web
8186
image: my-django-image:latest
8287
livenessProbe:
83-
exec:
84-
command:
85-
- python
86-
- manage.py
87-
- health_check
88-
- health_check-container
89-
- web:8000
88+
httpGet:
89+
path: /container/health/
90+
port: 8000
91+
httpHeaders:
92+
- name: X-Forwarded-Proto
93+
value: https
94+
- name: X-Forwarded-Host
95+
value: example.com # Use your actual domain from ALLOWED_HOSTS
9096
periodSeconds: 60
9197
timeoutSeconds: 10
9298
```
99+
100+
> [!TIP]
101+
> Configure `X-Forwarded-Host` to match your domain from `ALLOWED_HOSTS` and set `X-Forwarded-Proto` to `https` if your application enforces SSL.
102+
> See Django's [USE_X_FORWARDED_HOST](https://docs.djangoproject.com/en/stable/ref/settings/#std-setting-USE_X_FORWARDED_HOST) and [SECURE_PROXY_SSL_HEADER](https://docs.djangoproject.com/en/stable/ref/settings/#secure-proxy-ssl-header) settings.
Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
1+
import os
12
import sys
23
import urllib.error
34
import urllib.request
45

6+
from django.conf import settings
57
from django.core.management.base import BaseCommand
6-
from django.urls import reverse
8+
from django.urls import NoReverseMatch, reverse
79

810

911
class Command(BaseCommand):
1012
help = "Run health checks and exit 0 if everything went well."
1113

14+
@property
15+
def default_forwarded_host(self):
16+
return (
17+
settings.ALLOWED_HOSTS[0].strip(".")
18+
if settings.ALLOWED_HOSTS and settings.ALLOWED_HOSTS[0] != "*"
19+
else None
20+
)
21+
22+
@property
23+
def default_addrport(self):
24+
return ":".join([os.getenv("HOST", "127.0.0.1"), os.getenv("PORT", "8000")])
25+
1226
def add_arguments(self, parser):
1327
parser.add_argument(
1428
"endpoint",
@@ -19,28 +33,90 @@ def add_arguments(self, parser):
1933
"addrport",
2034
nargs="?",
2135
type=str,
22-
help="Optional port number, or ipaddr:port (default: localhost:8000)",
23-
default="localhost:8000",
36+
default=self.default_addrport,
37+
help=f"Optional port number, or ipaddr:port (default: :{self.default_addrport})",
38+
)
39+
parser.add_argument(
40+
"--forwarded-host",
41+
type=str,
42+
default=self.default_forwarded_host,
43+
help=f"Value for X-Forwarded-Host header (default: {self.default_forwarded_host})",
44+
)
45+
parser.add_argument(
46+
"--forwarded-proto",
47+
type=str,
48+
choices=["http", "https"],
49+
default="https",
50+
help="Value for X-Forwarded-Proto header (default: https)",
51+
)
52+
parser.add_argument(
53+
"--timeout",
54+
type=int,
55+
default=5,
56+
help="Timeout in seconds for the health check request (default: 5 seconds)",
2457
)
2558

2659
def handle(self, *args, **options):
2760
endpoint = options.get("endpoint")
28-
path = reverse(endpoint)
29-
host, sep, port = options.get("addrport").partition(":")
30-
url = f"http://{host}:{port}{path}" if sep else f"http://{host}{path}"
31-
request = urllib.request.Request( # noqa: S310
32-
url, headers={"Accept": "text/plain"}
61+
try:
62+
path = reverse(endpoint)
63+
except NoReverseMatch as e:
64+
self.stderr.write(
65+
f"Could not resolve endpoint {endpoint!r}: {e}\n"
66+
"Please provide a valid URL pattern name for the health check endpoint."
67+
)
68+
sys.exit(2)
69+
addrport = options.get("addrport")
70+
proto = (
71+
"https"
72+
if settings.SECURE_SSL_REDIRECT and not settings.USE_X_FORWARDED_HOST
73+
else "http"
3374
)
75+
url = f"{proto}://{addrport}{path}"
76+
77+
headers = {"Accept": "text/plain"}
78+
79+
# Add X-Forwarded-Host header
80+
if forwarded_host := options.get("forwarded_host"):
81+
headers["X-Forwarded-Host"] = forwarded_host
82+
83+
# Add X-Forwarded-Proto header
84+
if forwarded_proto := options.get("forwarded_proto"):
85+
headers["X-Forwarded-Proto"] = forwarded_proto
86+
87+
if options.get("verbosity", 1) >= 2:
88+
self.stdout.write(
89+
f"Checking health endpoint at {url!r} with headers: {headers}"
90+
)
91+
92+
request = urllib.request.Request(url, headers=headers) # noqa: S310
3493
try:
35-
response = urllib.request.urlopen(request) # noqa: S310
94+
response = urllib.request.urlopen(request, timeout=options["timeout"]) # noqa: S310
3695
except urllib.error.HTTPError as e:
37-
# 500 status codes will raise HTTPError
38-
self.stdout.write(e.read().decode("utf-8"))
39-
sys.exit(1)
96+
match e.code:
97+
case 500: # Health check failed
98+
self.stdout.write(e.read().decode("utf-8"))
99+
sys.exit(1)
100+
case 400:
101+
self.stderr.write(
102+
f'"{url}" is not reachable: {e.reason}\nPlease check your ALLOWED_HOSTS setting or use the --forwarded-host option.'
103+
)
104+
sys.exit(2)
105+
case _:
106+
self.stderr.write(
107+
"Unexpected HTTP error "
108+
f"when trying to reach {url!r}: {e}\n"
109+
f"You may have selected an invalid endpoint {endpoint!r}"
110+
f" or another application is running on {addrport!r}."
111+
)
112+
sys.exit(2)
40113
except urllib.error.URLError as e:
41114
self.stderr.write(
42-
f'"{url}" is not reachable: {e.reason}\nPlease check your ALLOWED_HOSTS setting.'
115+
f'"{url}" is not reachable: {e.reason}\nPlease check your server is running and reachable.'
43116
)
44117
sys.exit(2)
118+
except TimeoutError as e:
119+
self.stderr.write(f"Timeout when trying to reach {url!r}: {e}")
120+
sys.exit(2)
45121
else:
46122
self.stdout.write(response.read().decode("utf-8"))

tests/test_management.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,39 @@ def test_handle__url_error__connection_refused(self):
6464
assert exc_info.value.code == 2
6565
error_output = stderr.getvalue()
6666
assert "not reachable" in error_output
67+
68+
def test_handle__forwarded_host(self, live_server):
69+
"""Set X-Forwarded-Host header when --forwarded-host is provided."""
70+
parsed = urlparse(live_server.url)
71+
addrport = f"{parsed.hostname}:{parsed.port}"
72+
73+
stdout = StringIO()
74+
stderr = StringIO()
75+
call_command(
76+
"health_check",
77+
"health_check_test",
78+
addrport,
79+
forwarded_host="example.com",
80+
stdout=stdout,
81+
stderr=stderr,
82+
)
83+
output = stdout.getvalue()
84+
assert "OK" in output or "working" in output
85+
86+
def test_handle__forwarded_proto(self, live_server):
87+
"""Set X-Forwarded-Proto header when --forwarded-proto is provided."""
88+
parsed = urlparse(live_server.url)
89+
addrport = f"{parsed.hostname}:{parsed.port}"
90+
91+
stdout = StringIO()
92+
stderr = StringIO()
93+
call_command(
94+
"health_check",
95+
"health_check_test",
96+
addrport,
97+
forwarded_proto="https",
98+
stdout=stdout,
99+
stderr=stderr,
100+
)
101+
output = stdout.getvalue()
102+
assert "OK" in output or "working" in output

0 commit comments

Comments
 (0)