Skip to content

Commit 1f7fdd6

Browse files
authored
fix: follow the redirect the detector's ingress answers with (#14)
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.
1 parent 6719ba3 commit 1f7fdd6

3 files changed

Lines changed: 47 additions & 2 deletions

File tree

app/adapters/detector_readiness.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ def http_probe(client: HttpClientLike, url: str) -> Callable[[], Awaitable[bool]
3232
"""
3333

3434
async def probe() -> bool:
35-
return (await client.get(url)).status_code == 200
35+
status = (await client.get(url)).status_code
36+
if status != 200:
37+
# A redirect, a rejection and a service still starting are all "not
38+
# ready" but call for different responses, so the status is kept.
39+
logger.info("detector_probe_status", extra={"context": {"status": status}})
40+
return status == 200
3641

3742
return probe
3843

app/worker.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,11 @@ async def main() -> None:
136136

137137
# The first request also activates the detector, since it scales to zero,
138138
# so probing both starts it and establishes when it is usable.
139-
http = httpx.AsyncClient(timeout=DETECTOR_PROBE_TIMEOUT_S)
139+
# follow_redirects, because the detector's ingress refuses plain HTTP and
140+
# answers with a redirect to HTTPS. A client that does not follow it sees a
141+
# non-200 forever, and the redirect alone does not start an app scaled to
142+
# zero, so the endpoint would never become reachable.
143+
http = httpx.AsyncClient(timeout=DETECTOR_PROBE_TIMEOUT_S, follow_redirects=True)
140144
probe = http_probe(http, f"{settings.llm_guardrail_base_url.rstrip('/')}/models")
141145

142146
async def detector_ready() -> bool:

tests/unit/test_detector_readiness.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,39 @@ async def probe() -> bool:
158158

159159
assert "detector_not_ready_yet" in events
160160
assert "detector_ready" in events
161+
162+
163+
@pytest.mark.asyncio
164+
async def test_a_probe_reports_the_status_it_received():
165+
""" "Not ready" covers a redirect, a rejection and a service still starting,
166+
which need different responses. The status is recorded so the logs say
167+
which one it was."""
168+
import logging
169+
170+
from app.adapters.detector_readiness import http_probe
171+
172+
events: list[dict] = []
173+
174+
class _Collect(logging.Handler):
175+
def emit(self, record: logging.LogRecord) -> None:
176+
events.append(getattr(record, "context", {}))
177+
178+
class _Response:
179+
status_code = 307
180+
181+
class _Client:
182+
async def get(self, url: str):
183+
return _Response()
184+
185+
logger = logging.getLogger("screen")
186+
handler = _Collect()
187+
logger.addHandler(handler)
188+
previous = logger.level
189+
logger.setLevel(logging.INFO)
190+
try:
191+
assert not await http_probe(_Client(), "http://detector/v1/models")()
192+
finally:
193+
logger.removeHandler(handler)
194+
logger.setLevel(previous)
195+
196+
assert any(c.get("status") == 307 for c in events)

0 commit comments

Comments
 (0)