diff --git a/src/gondulf/middleware/https_enforcement.py b/src/gondulf/middleware/https_enforcement.py index 3d0bafb..659187b 100644 --- a/src/gondulf/middleware/https_enforcement.py +++ b/src/gondulf/middleware/https_enforcement.py @@ -12,6 +12,11 @@ from gondulf.config import Config logger = logging.getLogger("gondulf.middleware.https_enforcement") +# Internal endpoints exempt from HTTPS enforcement +# These are called by Docker health checks, load balancers, and monitoring systems +# that connect directly to the container without going through the reverse proxy. +HTTPS_EXEMPT_PATHS = {"/health", "/metrics"} + def is_https_request(request: Request) -> bool: """ @@ -93,6 +98,12 @@ class HTTPSEnforcementMiddleware(BaseHTTPMiddleware): # Continue processing return await call_next(request) + # Exempt internal endpoints from HTTPS enforcement + # These are used by Docker health checks, load balancers, etc. + # that connect directly without going through the reverse proxy. + if request.url.path in HTTPS_EXEMPT_PATHS: + return await call_next(request) + # Production mode: Enforce HTTPS if not is_https_request(request): logger.warning( diff --git a/tests/integration/test_https_enforcement.py b/tests/integration/test_https_enforcement.py index 90bcf63..e5b6e9e 100644 --- a/tests/integration/test_https_enforcement.py +++ b/tests/integration/test_https_enforcement.py @@ -67,3 +67,66 @@ class TestHTTPSEnforcement: response = client.get("/") # TestClient doesn't enforce HTTPS, but middleware should allow it assert response.status_code == 200 + + def test_health_endpoint_exempt_from_https_in_production( + self, client, monkeypatch + ): + """Test /health endpoint is accessible via HTTP in production mode. + + Docker health checks and load balancers call the health endpoint directly + without going through the reverse proxy, so it must work over HTTP. + The key assertion is that we don't get a 301 redirect to HTTPS. + """ + from gondulf.config import Config + + monkeypatch.setattr(Config, "DEBUG", False) + monkeypatch.setattr(Config, "TRUST_PROXY", False) + + # HTTP request to /health should NOT redirect to HTTPS + response = client.get( + "http://localhost:8000/health", follow_redirects=False + ) + # Should NOT be 301 redirect - actual status depends on DB state (200/503) + assert response.status_code != 301 + # Verify it reached the health endpoint (not redirected) + assert response.status_code in (200, 503) + + def test_health_endpoint_head_request_in_production(self, client, monkeypatch): + """Test HEAD request to /health is not redirected in production. + + Docker health checks may use HEAD requests. The key is that the + middleware doesn't redirect to HTTPS - the actual endpoint behavior + (405 Method Not Allowed) is separate from HTTPS enforcement. + """ + from gondulf.config import Config + + monkeypatch.setattr(Config, "DEBUG", False) + monkeypatch.setattr(Config, "TRUST_PROXY", False) + + # HEAD request to /health should NOT redirect to HTTPS + response = client.head( + "http://localhost:8000/health", follow_redirects=False + ) + # Should NOT be 301 redirect + assert response.status_code != 301 + + def test_metrics_endpoint_exempt_from_https_in_production( + self, client, monkeypatch + ): + """Test /metrics endpoint is accessible via HTTP in production mode. + + Monitoring systems may call metrics directly without HTTPS. + """ + from gondulf.config import Config + + monkeypatch.setattr(Config, "DEBUG", False) + monkeypatch.setattr(Config, "TRUST_PROXY", False) + + # HTTP request to /metrics should not be redirected + # (endpoint may not exist yet, but should not redirect to HTTPS) + response = client.get( + "http://localhost:8000/metrics", follow_redirects=False + ) + # Should return 404 (not found) not 301 (redirect to HTTPS) + assert response.status_code != 301 +