Every route answers HEAD where upstream answers 405 #136

Closed
opened 2026-08-14 07:47:22 +02:00 by Claude · 0 comments
Collaborator

axum's get() serves HEAD implicitly; ASP.NET serves it only where [HttpHead] is declared. So this server answers HEAD on routes upstream refuses as a method, port-wide.

Measured while closing #131:

HEAD /Videos/{id}/main.m3u8      upstream 405 (Allow: GET)   ours 200 (before), 401 (after #131)
HEAD /Items/{id}                 upstream 405                ours 200
HEAD /System/Info/Public         upstream 405 (Allow: GET)   ours 200

#131 fixed the four inside crates/jellyfin-http/src/routes/streaming/hls.rs by declaring the methods each action actually has, which is the right shape for one module and the wrong shape for 253 registrations.

Where to start

The list of upstream's [HttpHead] declarations is the input, and it is derivable rather than transcribable: xtask endpoints already scans upstream's controllers to generate ENDPOINTS.md, so extending that scanner to record which actions declare [HttpHead] gives the answer key. Then the router declares GET-only where upstream does.

ENDPOINTS.md already counts HEAD operations separately — it lists 37 image routes with GET and HEAD as distinct rows — so the generated inventory is the natural place for this to be checked rather than remembered.

Two details worth carrying, both from #131's measurements:

  • Upstream's header is Allow: GET, HEAD with a space; ours writes Allow: GET,HEAD.
  • A method refusal happens before authorization: upstream answers 405 to an anonymous HEAD and to one carrying a token alike, where a route it does declare HEAD on answers 401 anonymously.

How to know it is done

A HEAD to every route this server declares answers what upstream answers — which is a comparison xtask endpoints can make once it knows upstream's declarations, and refusals.json is where a handful of representative ones belong so a replay holds it.

axum's `get()` serves HEAD implicitly; ASP.NET serves it only where `[HttpHead]` is declared. So this server answers HEAD on routes upstream refuses as a method, port-wide. Measured while closing #131: ``` HEAD /Videos/{id}/main.m3u8 upstream 405 (Allow: GET) ours 200 (before), 401 (after #131) HEAD /Items/{id} upstream 405 ours 200 HEAD /System/Info/Public upstream 405 (Allow: GET) ours 200 ``` #131 fixed the four inside `crates/jellyfin-http/src/routes/streaming/hls.rs` by declaring the methods each action actually has, which is the right shape for one module and the wrong shape for 253 registrations. ## Where to start The list of upstream's `[HttpHead]` declarations is the input, and it is derivable rather than transcribable: `xtask endpoints` already scans upstream's controllers to generate `ENDPOINTS.md`, so extending that scanner to record which actions declare `[HttpHead]` gives the answer key. Then the router declares GET-only where upstream does. `ENDPOINTS.md` already counts HEAD operations separately — it lists 37 image routes with `GET` and `HEAD` as distinct rows — so the generated inventory is the natural place for this to be checked rather than remembered. Two details worth carrying, both from #131's measurements: - Upstream's header is `Allow: GET, HEAD` with a space; ours writes `Allow: GET,HEAD`. - A method refusal happens **before** authorization: upstream answers 405 to an anonymous HEAD and to one carrying a token alike, where a route it does declare HEAD on answers 401 anonymously. ## How to know it is done A HEAD to every route this server declares answers what upstream answers — which is a comparison `xtask endpoints` can make once it knows upstream's declarations, and `refusals.json` is where a handful of representative ones belong so a replay holds it.
alex closed this issue 2026-08-15 21:24:16 +02:00
Sign in to join this conversation.
No description provided.