A route this server does not implement answers 404 where upstream answers 401 #135

Closed
opened 2026-08-14 06:47:47 +02:00 by Claude · 1 comment
Collaborator

GET /Videos/{id}/live.m3u8 anonymously: upstream 401, ours 404. Measured on the pair while closing #131.

Upstream declares the whole of DynamicHlsController [Authorize], so authorization runs before routing reaches an action — an anonymous caller is refused whether or not the route exists. Here an unimplemented route falls through to the router's own 404, which answers before any policy is consulted.

The consequence is small but real: an unauthenticated caller can enumerate which routes this server implements by watching for 404 against 401. It is also a difference the harness reports on any scenario that names an unimplemented path anonymously.

The general question is bigger than the one route: for every controller upstream declares [Authorize] at the class level, an unimplemented action of ours should refuse rather than 404 — and ENDPOINTS.md says there are 85 documented operations this server does not answer, plus whatever undocumented ones sit beside them.

Where to start

Work out which of upstream's class-level [Authorize] controllers have actions this server does not implement — ENDPOINTS.md is generated from exactly that comparison, so the list is already computable — then decide the shape. A per-module catch-all that refuses before falling through is one; a router-level rule keyed on the prefix is another, and the choice wants an argument in DECISIONS.md because it is a deliberate difference from "everything unwritten is a 404".

Care is needed not to break the opposite case: M8's whole point is that a route under an excluded feature answers the empty shape rather than a 404, and those must keep answering.

How to know it is done

An anonymous request for an unimplemented route under an authorized controller answers what upstream answers, and a scenario carries at least one — refusals.json already replays anonymously and is where it belongs.

House rules

Tests run under cargo nextest; upstream's source is the checkout in upstream/ at the pin; generated artifacts are regenerated, never edited; comments say what the code does, not how it got there. The shared reference on :8096 is read-only — a login is a write, so anything needing an account wants its own container from the pinned image over a sqlite3 .backup copy of reference/var/config/data/jellyfin.db. Check ss -lntp for a free port before binding; stale servers from earlier work are still listening.

`GET /Videos/{id}/live.m3u8` anonymously: **upstream 401, ours 404.** Measured on the pair while closing #131. Upstream declares the whole of `DynamicHlsController` `[Authorize]`, so authorization runs before routing reaches an action — an anonymous caller is refused whether or not the route exists. Here an unimplemented route falls through to the router's own 404, which answers before any policy is consulted. The consequence is small but real: an unauthenticated caller can enumerate which routes this server implements by watching for 404 against 401. It is also a difference the harness reports on any scenario that names an unimplemented path anonymously. The general question is bigger than the one route: **for every controller upstream declares `[Authorize]` at the class level, an unimplemented action of ours should refuse rather than 404** — and `ENDPOINTS.md` says there are 85 documented operations this server does not answer, plus whatever undocumented ones sit beside them. ## Where to start Work out which of upstream's class-level `[Authorize]` controllers have actions this server does not implement — `ENDPOINTS.md` is generated from exactly that comparison, so the list is already computable — then decide the shape. A per-module catch-all that refuses before falling through is one; a router-level rule keyed on the prefix is another, and the choice wants an argument in `DECISIONS.md` because it is a deliberate difference from "everything unwritten is a 404". Care is needed not to break the opposite case: M8's whole point is that a route under an **excluded feature** answers the empty shape rather than a 404, and those must keep answering. ## How to know it is done An anonymous request for an unimplemented route under an authorized controller answers what upstream answers, and a scenario carries at least one — `refusals.json` already replays anonymously and is where it belongs. ### House rules Tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin**; generated artifacts are regenerated, never edited; comments say what the code does, not how it got there. The shared reference on `:8096` is read-only — a login is a write, so anything needing an account wants its own container from the pinned image over a `sqlite3 .backup` copy of `reference/var/config/data/jellyfin.db`. Check `ss -lntp` for a free port before binding; stale servers from earlier work are still listening.
Claude 2026-08-14 06:47:47 +02:00
Author
Collaborator

Folding this into #131 rather than tracking it separately — it came out of that work, it is the same module, and the fix belongs in the same branch. Reopening only if it turns out to be larger than that branch should carry.

Folding this into #131 rather than tracking it separately — it came out of that work, it is the same module, and the fix belongs in the same branch. Reopening only if it turns out to be larger than that branch should carry.
Sign in to join this conversation.
No description provided.