From 3c100c4d2f0de01ebe52785a1fff9bc215fa305a Mon Sep 17 00:00:00 2001 From: inesaranab Date: Tue, 11 Aug 2026 21:14:57 +0200 Subject: [PATCH] fix: follow the redirect the detector's ingress answers with The detector's ingress sets allowInsecure false, so a plain HTTP request is answered with a redirect to HTTPS rather than being served. The readiness probe used a client that does not follow redirects, so every attempt saw a non-200 and reported the endpoint as not serving. A redirect from the proxy does not start an app scaled to zero either, so the detector was never activated and the wait could only ever expire. The same URL worked from the guardrail because the OpenAI client follows redirects by default. The two clients disagreeing is what made this look like a networking problem rather than a client one. The probe now records the status it received. "Not ready" covers a redirect, a rejection and a service still starting, and telling them apart from the logs is what identified this. --- app/adapters/detector_readiness.py | 7 +++++- app/worker.py | 6 ++++- tests/unit/test_detector_readiness.py | 36 +++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/app/adapters/detector_readiness.py b/app/adapters/detector_readiness.py index d63433a..097f10e 100644 --- a/app/adapters/detector_readiness.py +++ b/app/adapters/detector_readiness.py @@ -32,7 +32,12 @@ def http_probe(client: HttpClientLike, url: str) -> Callable[[], Awaitable[bool] """ async def probe() -> bool: - return (await client.get(url)).status_code == 200 + status = (await client.get(url)).status_code + if status != 200: + # A redirect, a rejection and a service still starting are all "not + # ready" but call for different responses, so the status is kept. + logger.info("detector_probe_status", extra={"context": {"status": status}}) + return status == 200 return probe diff --git a/app/worker.py b/app/worker.py index 6d924d9..75e326d 100644 --- a/app/worker.py +++ b/app/worker.py @@ -136,7 +136,11 @@ async def main() -> None: # The first request also activates the detector, since it scales to zero, # so probing both starts it and establishes when it is usable. - http = httpx.AsyncClient(timeout=DETECTOR_PROBE_TIMEOUT_S) + # follow_redirects, because the detector's ingress refuses plain HTTP and + # answers with a redirect to HTTPS. A client that does not follow it sees a + # non-200 forever, and the redirect alone does not start an app scaled to + # zero, so the endpoint would never become reachable. + http = httpx.AsyncClient(timeout=DETECTOR_PROBE_TIMEOUT_S, follow_redirects=True) probe = http_probe(http, f"{settings.llm_guardrail_base_url.rstrip('/')}/models") async def detector_ready() -> bool: diff --git a/tests/unit/test_detector_readiness.py b/tests/unit/test_detector_readiness.py index efa9afc..81370a7 100644 --- a/tests/unit/test_detector_readiness.py +++ b/tests/unit/test_detector_readiness.py @@ -158,3 +158,39 @@ async def probe() -> bool: assert "detector_not_ready_yet" in events assert "detector_ready" in events + + +@pytest.mark.asyncio +async def test_a_probe_reports_the_status_it_received(): + """ "Not ready" covers a redirect, a rejection and a service still starting, + which need different responses. The status is recorded so the logs say + which one it was.""" + import logging + + from app.adapters.detector_readiness import http_probe + + events: list[dict] = [] + + class _Collect(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + events.append(getattr(record, "context", {})) + + class _Response: + status_code = 307 + + class _Client: + async def get(self, url: str): + return _Response() + + logger = logging.getLogger("screen") + handler = _Collect() + logger.addHandler(handler) + previous = logger.level + logger.setLevel(logging.INFO) + try: + assert not await http_probe(_Client(), "http://detector/v1/models")() + finally: + logger.removeHandler(handler) + logger.setLevel(previous) + + assert any(c.get("status") == 307 for c in events)