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)