Skip to content

Providers: wrong exception class escaping the lifecycle - #19

Merged
leeovery merged 8 commits into
fix2/migrationsfrom
fix2/providers
Aug 25, 2026
Merged

Providers: wrong exception class escaping the lifecycle#19
leeovery merged 8 commits into
fix2/migrationsfrom
fix2/providers

Conversation

@leeovery

@leeovery leeovery commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Seven confirmed provider-boundary defects from the sweep. The unifying theme: the wrong Python exception class escaped the intended lifecycle, landing as "engine bug, filed GitHub issue, frozen until the next release" where the design mandates a retryable block or a manual park.

  • Truncated HTTP bodieshttp.client.IncompleteRead is not an OSError, and it is the normal failure mode for 100MB+ podcast enclosures. Normalised at the transport seam, so all eight callers are fixed at once and a truncated read is blocked, not an engine error.
  • A one-slash typo in the whisper model name crashed every CLI verb — the availability probe called HuggingFace validation unguarded, and its own docstring says the probe must never raise. Verified against the real CLI: status, run and transcribe all exited 1.
  • A video with no audio stream raised IndexError out of whisper-local as an engine bug, while whisper-api mapped the same input correctly to manual — same input, opposite lifecycle, decided by which provider resolved.
  • csv-builtin let csv.Error escape on an oversized field, in exactly the fallback scenario it exists to serve. The field bound is now raised to 16MB with a stated refusal beyond it.
  • The whisper priming prompt overflowed the 224-token window from the front, discarding the title and show it was assembled to carry. Now budgeted head-first, with the vocabulary trimmed from its tail.
  • A CDN error page cached as .mp3 parked the episode manual forever where blocked is honest; empty, short-of-content-length and HTML-lead bodies are now blocked and never cached.
  • Provider notes reach the capability report on any state, duplicate provider names are refused, and the extract path handles ProviderUnavailableError as a waiting re-park.

Gates: 1087 tests, ruff and ty clean.

🤖 Generated with Claude Code

@leeovery leeovery changed the title fix2/providers Providers: wrong exception class escaping the lifecycle Aug 22, 2026
leeovery and others added 8 commits August 22, 2026 15:13
A server closing the socket mid-body raises http.client.IncompleteRead
out of response.read() — the routine failure mode for a 100MB podcast
enclosure. IncompleteRead is an HTTPException, not an OSError, so every
caller's `except OSError` connection guard missed it and a truncated
download landed as `error` with a filed issue, frozen until the next
engine release, where blocked-with-retries is the honest outcome.

Normalizing the whole http.client family into ConnectionError at the two
urllib seams (the driver transport and whisper-api's multipart POST)
fixes all eight call sites at once, including the media stage and the
enclosure download, and routes them through classify_connection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two escapes from the same provider. HuggingFace raises HFValidationError
from the cache lookup for repo-id shapes like `Systran/faster-whisper/
large-v3` that the name check waves through on its slash — and that probe
runs on every CLI verb, so a typo in transcribe_model crashed status, run
and transcribe outright. The cache check is now guarded: a name HF
rejects reports one unavailable provider, an unreadable cache says so.

And faster-whisper indexes a container's audio streams unguarded, so a
video-only file arrives as a bare IndexError — an engine bug with a filed
issue, where whisper-api already calls the same file manual. It maps to
ProviderInputError like every other bad input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
csv.Error escapes from the reader's iteration, not its construction, so
the comprehension building the rows let it through: a CSV field over the
stdlib's 128KB default — routine for an embedded JSON blob — landed as an
engine bug with an issue filed.

A big field is not a malformed file, so the bound is raised to 16MB
rather than the failure merely being caught. It stays finite: one
unterminated quote in a file that only looked like CSV makes the whole
file a single field, and a bound turns that into a stated manual instead
of eating the machine's memory. Anything the reader still refuses is
ProviderInputError. The limit is process-global, so it is restored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Whisper keeps a prompt's LAST ~224 tokens and discards the front, so an
800-char priming string (~270 tokens of jargon-dense text) threw away the
title and show it exists to carry. The composed prompt is now budgeted to
~200 tokens: the head survives whole, the vocabulary is trimmed from its
own tail, and whisper-api's continuity tail shares that budget instead of
being appended past it.

Second, a CDN error page cached as <hash>.mp3 decoded as garbage and
parked the episode manual forever. An empty body, a body short of its
declared Content-Length, or an HTML page where audio was expected is now
blocked and never written under the audio name — nothing about the
episode was learned. Bytes that are merely bad audio still reach the
provider and its manual stands.

The file driver's HTML-lead sniff moves to pipeline/detect, where the
byte sniffing lives, so both callers read the same bytes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The capability report dropped an ok-with-caveat note for available and
active providers, so "model not cached — the first transcription
downloads it" never reached the surface built to explain it; the note now
rides any state, and the renderer prints it on the active row too.

Duplicate provider names in config passed through, probing the provider
twice and printing it twice on the report — refused now, as loudly as an
unknown name.

The extract dispatch handled ProviderInputError and ScannedDocumentError
but not ProviderUnavailableError, so a contract-honoring extract provider
that failed at call time got error plus a filed issue where the
transcribe path re-parks waiting. It re-parks too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The multipart POST is the second urllib seam wrapped by the transport
normalization; nothing asserted it, so a future edit could drop the guard
and send truncated uploads back to the engine-bug path.

The socket double now reads the whole request — headers and body — before
answering: replying mid-upload closes the socket on unread bytes and the
client sees a reset rather than the truncated read under test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The head-first character budget was plain truncation wearing a docstring:
the head already sat at the FRONT of the composed string, so `text[:600]`
kept it either way, and the claim that truncating instead "would keep 600
characters of show notes and throw away the two names" was false.

Worse, whisper counts its window in TOKENS. 600 characters is ~180 tokens
of Cyrillic and ~690 of Chinese, and past 223 tokens whisper keeps only
`previous_tokens[-(448 // 2 - 1):]` — it discards the FRONT. For every CJK
or Cyrillic item the title was therefore the first thing lost, by the code
written to save it, and whisper-api's chunk continuity compounded it.

Two changes. The title/show now goes at the END of the prompt, behind the
vocabulary: whatever overflows the window is show notes, by construction,
in any script. And the budget is spent through a per-character estimate of
what whisper's BPE charges, measured against openai/whisper-tiny's
tokenizer and rounded up per script family — so a prompt does not overflow
in the first place.

The tokenizer itself is not loaded: that means a HuggingFace fetch inside
a step that primes remote providers needing no local model, to buy an
exactness the head's placement already made unnecessary. A mis-estimate
now costs vocabulary, not names. A live-marked test checks the estimate
against the real tokenizer in nine scripts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three sites of one defect. The transport's error-body read already
suppressed `http.client.HTTPException` and nothing pinned it: with the
suppress narrowed to OSError the whole suite stayed green, while a real
socket serving a truncated 404 turned the item's `dead` into `blocked` —
the read failure escaping through `normalize_httplib_errors` as a
connection error. That case is now a socket test.

whisper-api's multipart POST, twenty lines from its sibling, had no such
guard at all: a 400 whose body stopped short of its Content-Length raised
out of `e.read()`, and `_transcribe_chunk`'s `except OSError` filed it as
ProviderUnavailableError. The episode then waited forever on an endpoint
that was answering fine, instead of parking manual under the escalation
clock the bad audio had earned. The two seams are symmetric now, both
pinned against a socket that hangs up mid-body.

dex-inbox's two urllib sites had neither the guard nor a caller that could
report what escaped: `main` states OSError, ValueError and RuntimeError,
and an IncompleteRead is none of them, so a GitHub asset download cut
mid-body reached the operator as a traceback. Both sites go through the
seam's guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@leeovery
leeovery marked this pull request as ready for review August 22, 2026 14:14
@leeovery
leeovery merged commit ca8ef98 into main Aug 25, 2026
@leeovery
leeovery deleted the fix2/providers branch August 25, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant