Give an expired subtitle candidate a typed error #221

Closed
opened 2026-08-25 02:51:24 +01:00 by naps62-yolo · 1 comment
Owner

A provider candidate id does not survive the search that produced it:
OpenSubtitles caches a candidate's language and format at search time and
answers NotFound for an id from an earlier search (see the integration note
on #187).

#203's manual panel copes by appending "search again" to the failure text
when the upstream detail string matches not found. That is string
matching on a message a provider controls and can reword at any time, in the
one place the operator is told what to do next.

Give the failure a typed shape instead. Either:

  • an error code on the API response (candidate_expired) that the panel
    switches on, or
  • a search token the grab must present, so an id from a stale search is
    rejected by arr rather than by the provider.

The second also fixes the case where a provider happens to reuse an id.

Found while implementing #203.

Depends on: #187, #199, #203

A provider candidate id does not survive the search that produced it: OpenSubtitles caches a candidate's language and format at search time and answers `NotFound` for an id from an earlier search (see the integration note on #187). #203's manual panel copes by appending "search again" to the failure text **when the upstream detail string matches `not found`**. That is string matching on a message a provider controls and can reword at any time, in the one place the operator is told what to do next. Give the failure a typed shape instead. Either: - an error code on the API response (`candidate_expired`) that the panel switches on, or - a search token the grab must present, so an id from a stale search is rejected by arr rather than by the provider. The second also fixes the case where a provider happens to reuse an id. Found while implementing #203. Depends on: #187, #199, #203
naps62-yolo added this to the Subtitles milestone 2026-08-25 02:51:24 +01:00
naps62-yolo added the area/apidifficulty/easytype/bugphase/9-subtitles labels 2026-08-25 02:51:24 +01:00
Author
Owner

Implemented option 1 from the issue: a typed error rather than a search token. The design doc's own comment at the top of subtitles.rs states the server deliberately remembers nothing about a search, so option 2 (a search token the grab must present) would have reintroduced server-side search state against that stated invariant. A typed error needs no such state.

grab() now distinguishes arr_subs::Error::NotFound from other provider failures and returns a new ApiError::SubtitleCandidateExpired, which serializes as HTTP 404 with ErrorBody { error, code: "candidate_expired" }. ErrorBody gained an optional code field (None everywhere else) so this is additive, not a breaking change to the existing error shape. The web panel (fetchCandidate in main.ts) now switches on outcome.code === "candidate_expired" instead of regex-matching the upstream message text for /not found/i. Added a server-side test (a_grab_of_an_expired_candidate_is_a_typed_404) using the existing DeadProvider fixture, which already returned Error::NotFound.

Branch: subtitles/221-candidate-error
just ci: passed (686 tests, fmt, clippy, web typecheck/lint, check-tokens all clean).

Findings

None outside this issue's scope.

Implemented option 1 from the issue: a typed error rather than a search token. The design doc's own comment at the top of `subtitles.rs` states the server deliberately remembers nothing about a search, so option 2 (a search token the grab must present) would have reintroduced server-side search state against that stated invariant. A typed error needs no such state. `grab()` now distinguishes `arr_subs::Error::NotFound` from other provider failures and returns a new `ApiError::SubtitleCandidateExpired`, which serializes as HTTP 404 with `ErrorBody { error, code: "candidate_expired" }`. `ErrorBody` gained an optional `code` field (`None` everywhere else) so this is additive, not a breaking change to the existing error shape. The web panel (`fetchCandidate` in `main.ts`) now switches on `outcome.code === "candidate_expired"` instead of regex-matching the upstream message text for `/not found/i`. Added a server-side test (`a_grab_of_an_expired_candidate_is_a_typed_404`) using the existing `DeadProvider` fixture, which already returned `Error::NotFound`. Branch: `subtitles/221-candidate-error` `just ci`: passed (686 tests, fmt, clippy, web typecheck/lint, check-tokens all clean). ## Findings None outside this issue's scope.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yolo/arr#221