A dropped connection during synthesis is an outage too, and the ElevenLabs status split lives in one place - #548
Conversation
…tage too synthesize_speech's own TTS post was the one ElevenLabs call in the repo still unwrapped: every other caller (transcribe_audio, platform/stt, platform/tts, project_health's elevenlabs_client) already turns a dropped connection or a read timeout into UpstreamServiceError, but this one let httpx.HTTPError rise raw past the status-code check that only fires once a response exists — so a network failure to ElevenLabs answered the client as a generic 500 instead of the 502 every other path here already gives. ENG-1093 (#542) scoped this gap out on purpose: it wrapped project_health's two calls and widened the status split everywhere but named synthesize_speech's TTS post as the one still open, tracked for a follow-up. This closes it, with the same try/except httpx.HTTPError shape the other four already use. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… not three translation_helper/synthesize_speech.py, translation_helper/transcribe_audio.py and project_health/voice/elevenlabs_client.py each carried their own copy of the same status-to-exception mapping (401/403/429 or >=500 -> UpstreamServiceError, other 4xx -> ValidationError), widened to agree by ENG-1093/#542 but never merged. app/core/exceptions.py already owns both classes and every one of these callers already imports from it, so the shared function goes there instead of a new module, with the docstring transcribe_audio.py carried since it names the rationale (401/403 is not silence any more than a rate limit is). The shared helper takes the message explicitly rather than building one string for every caller, so each site keeps the exact wording it had ("TTS request failed with status..." vs "Transcription request failed..."). No behaviour changes here: every caller's own tests, unmodified, still pass, and each still raises the same exception type with the same message for the same status code as before. platform/stt.py and platform/tts.py still keep their own narrower copy (429 or >=500 only, no 401/403) — collapsing those into the same split is a real behaviour change and lands as its own commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lure too platform/stt.py and platform/tts.py were the two ElevenLabs callers still splitting status codes the old way (429 or >=500 only): 401 and 403 read as ValidationError there, a bad-request 400 for a key ElevenLabs itself revoked or a quota it says is spent. The other three callers already widened this to 401/403/429/>=500 in ENG-1093/#542, and bed7bec's own body named platform/tts as the copy left untouched. Both now call the shared app.core.exceptions.upstream_or_validation_error, closing the last two copies of this split — one definition, five callers. Falsified per file: narrowing the shared helper back to 429-and-above made each file's own new 401/403 cases fail with ValidationError again; restoring it turned both files, and the whole suite, green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Read the whole diff: the core helper, the five clients, the three test files.
Checked that the refactor is behaviour-preserving where it claims to be — every message string survives verbatim, the three wide callers keep the wide rule, and grep leaves exactly one definition of the split in app/. The new transport catch at app/services/translation_helper/synthesize_speech.py:374 is byte-identical to the five siblings it joins. The two platform widenings land where the body says they do: nothing in app/ catches ValidationError around either call, and app/api/platform/stt.py:82 wraps the cleanup rather than the transcription, so a revoked key really does answer 502 now.
I could not run the suite from this environment, so the green run and the new parametrised cases stand unverified from here — CI settles that.
Nothing to raise. That is the whole set for this PR.
translation_helper/synthesize_speech.pyposted to ElevenLabs with no catch around it: a dropped connection or a read timeout rose raw, a generic 500, where the other four clients answerUpstreamServiceError(502). It now catcheshttpx.HTTPErrorthe way they do.Five near-identical copies of the status-to-exception split remained after ENG-1093, three on the wide rule (401, 403, 429 or ≥ 500 are the upstream's failure; other 4xx are ours) and two,
platform/stt.pyandplatform/tts.py, still on the narrow one, so a revoked key or a spent quota on the platform routes answered 400. The split lives once now, inapp/core/exceptions.pybeside the two classes it returns, with the docstring that said why 401 and 403 belong upstream; every client reads it with its own message unchanged, and the two platform routes answer 502 for a revoked key like the rest.Verificação: 3 commits (transporte; helper compartilhado, refactor puro; stt/tts alargados); teste novo parametrizado ConnectError/ReadTimeout em
test_th_audio_service.py(vermelho sem o fix); testes 401/403 emtest_platform/test_stt.pyetest_tts_service.py(vermelhos sem o alargamento); grep: uma definição do split; suíte dos cinco chamadores (110) e suíte inteira verdes; ruff, mypy limpos; revisora (poço-lupa) sem achados.🤖 Generated with Claude Code