A profile picture cannot be set: the legacy user-image routes are missing #165

Closed
opened 2026-08-21 09:57:47 +02:00 by Claude · 1 comment
Collaborator

Setting a profile picture from the pinned web bundle does nothing: the file
dialog takes the image, the preview updates, and the avatar is never stored.
Nothing appears in the log either, because a request that matches no route is
not traced — the miss is invisible from both ends.

The bundle uploads to a spelling we do not answer.
jellyfin-apiclient's uploadUserImage posts the base64 body to
Users/{userId}/Images/{imageType} with the file's content type, and
deleteUserImage sends DELETE to the same path. user-userprofile's image
picker calls both. We declare /UserImage alone —
crates/ephyra-http/src/routes/library/images/mod.rs:133.

POST   /Users/{id}/Images/Primary  -> 404
DELETE /Users/{id}/Images/Primary  -> 404
GET    /Users/{id}/Images/Primary  -> 404
POST   /UserImage                  -> 401

Upstream answers both spellings. Jellyfin.Api/Controllers/ImageController.cs
declares the legacy user-image routes at 159 and 182 (POST), 251 and 274
(DELETE), and 1554 and 1610 (GET/HEAD) — each one
[Obsolete("Kept for backwards compatibility")] and each one
[ApiExplorerSettings(IgnoreApi = true)]. They forward to the /UserImage
action, so the behaviour is already written here; only the spellings are
missing.

The spec cannot see this class of route, so nothing here counts it.
IgnoreApi keeps them out of upstream's own generator, so
crates/ephyra-dto/openapi.json has no Users/{userId}/Images at all, and
ENDPOINTS.md — which is generated from that spec — neither lists them as
answered nor as unanswered. This is the gap ENDPOINTS.md already names in
prose: the legacy /Users/{userId}/… spellings the pinned bundle still calls.
Sixteen of that family are served (crates/ephyra-http/src/canonical.rs:293),
and the four image ones are the hole in it.

Where to start

The three legacy paths route to the handlers already in
routes/library/images/account.rs; the user is the route parameter rather than
the query. canonical.rs needs the entries too, or the methods it refuses on
their behalf will be axum's rather than upstream's.

Worth doing in the same pass: sweep the pinned bundle for the other paths it
asks for that the spec never declared, so the next one of these is found by a
test rather than by somebody clicking. ENDPOINTS.md counts 37 answered but
undocumented, and that number came from reading upstream rather than from
asking the client what it calls.

How to know it is done

Choosing an image on the user profile page stores it, the avatar comes back on
the next load, and deleting it removes it — all with the pinned bundle and no
other client. A test that asks the legacy path and fails today.

House rules

CLAUDE.md is the short version and these are the parts that bite: tests run
under cargo nextest; upstream's source is the checkout in upstream/ at the
pin
; generated files are regenerated rather than edited; a comment says what
the code does, not how it got there.

A test that cannot fail is worse than none.

Setting a profile picture from the pinned web bundle does nothing: the file dialog takes the image, the preview updates, and the avatar is never stored. Nothing appears in the log either, because a request that matches no route is not traced — the miss is invisible from both ends. **The bundle uploads to a spelling we do not answer.** `jellyfin-apiclient`'s `uploadUserImage` posts the base64 body to `Users/{userId}/Images/{imageType}` with the file's content type, and `deleteUserImage` sends `DELETE` to the same path. `user-userprofile`'s image picker calls both. We declare `/UserImage` alone — `crates/ephyra-http/src/routes/library/images/mod.rs:133`. POST /Users/{id}/Images/Primary -> 404 DELETE /Users/{id}/Images/Primary -> 404 GET /Users/{id}/Images/Primary -> 404 POST /UserImage -> 401 **Upstream answers both spellings.** `Jellyfin.Api/Controllers/ImageController.cs` declares the legacy user-image routes at 159 and 182 (`POST`), 251 and 274 (`DELETE`), and 1554 and 1610 (`GET`/`HEAD`) — each one `[Obsolete("Kept for backwards compatibility")]` and each one `[ApiExplorerSettings(IgnoreApi = true)]`. They forward to the `/UserImage` action, so the behaviour is already written here; only the spellings are missing. **The spec cannot see this class of route, so nothing here counts it.** `IgnoreApi` keeps them out of upstream's own generator, so `crates/ephyra-dto/openapi.json` has no `Users/{userId}/Images` at all, and `ENDPOINTS.md` — which is generated from that spec — neither lists them as answered nor as unanswered. This is the gap `ENDPOINTS.md` already names in prose: the legacy `/Users/{userId}/…` spellings the pinned bundle still calls. Sixteen of that family are served (`crates/ephyra-http/src/canonical.rs:293`), and the four image ones are the hole in it. ## Where to start The three legacy paths route to the handlers already in `routes/library/images/account.rs`; the user is the route parameter rather than the query. `canonical.rs` needs the entries too, or the methods it refuses on their behalf will be `axum`'s rather than upstream's. Worth doing in the same pass: sweep the pinned bundle for the other paths it asks for that the spec never declared, so the next one of these is found by a test rather than by somebody clicking. `ENDPOINTS.md` counts 37 answered but undocumented, and that number came from reading upstream rather than from asking the client what it calls. ## How to know it is done Choosing an image on the user profile page stores it, the avatar comes back on the next load, and deleting it removes it — all with the pinned bundle and no other client. A test that asks the legacy path and fails today. ### House rules `CLAUDE.md` is the short version and these are the parts that bite: tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin**; generated files are regenerated rather than edited; a comment says what the code does, not how it got there. A test that cannot fail is worse than none.
Author
Collaborator

Landed in 73e5d4e.

The three legacy paths route to the handlers that were already in
routes/library/images/account.rs — the account comes off the path rather than
the query, and the type and index are bound and ignored, as they are upstream.
canonical.rs carries both spellings with their methods, or the ones refused on
their behalf would be axum's rather than upstream's. ENDPOINTS.md counts eight
more answered-but-undocumented, which is exactly this family (37 → 45).

A test covers the round trip at the legacy spelling: nothing stored is a 404,
a POST writes the same file /UserImage writes, the GET serves it back, the
indexed spelling answers the same picture, and a DELETE removes it.

On the sweep

The suggestion to sweep the bundle for other undeclared paths in the same pass
does not come out mechanically, and it is worth recording why rather than
leaving it looking undone:

  • The bundle builds paths by concatenation, not template literals —
    "Users/" + userId + "/Images/" + type. grep over the minified chunks finds
    "Users/" 29 times and nothing that resembles a whole route, so a static scan
    cannot follow it.
  • Checking the recorded traffic instead: every one of bundle.json's 40
    distinct paths is already declared in canonical.rs. That scenario is home
    screen traffic, which is why it never carried the user-image routes.

So finding the next one of these needs more recorded traffic — the profile page,
the library dialog, the picker — rather than a scan of what is in the tree. That
is a scenario-authoring job, and it is the shape bundle.json already has.

Landed in 73e5d4e. The three legacy paths route to the handlers that were already in `routes/library/images/account.rs` — the account comes off the path rather than the query, and the type and index are bound and ignored, as they are upstream. `canonical.rs` carries both spellings with their methods, or the ones refused on their behalf would be axum's rather than upstream's. `ENDPOINTS.md` counts eight more answered-but-undocumented, which is exactly this family (37 → 45). A test covers the round trip at the legacy spelling: nothing stored is a 404, a POST writes the same file `/UserImage` writes, the GET serves it back, the indexed spelling answers the same picture, and a DELETE removes it. ## On the sweep The suggestion to sweep the bundle for other undeclared paths in the same pass does not come out mechanically, and it is worth recording why rather than leaving it looking undone: - The bundle builds paths by **concatenation**, not template literals — `"Users/" + userId + "/Images/" + type`. `grep` over the minified chunks finds `"Users/"` 29 times and nothing that resembles a whole route, so a static scan cannot follow it. - Checking the recorded traffic instead: every one of `bundle.json`'s 40 distinct paths is already declared in `canonical.rs`. That scenario is home screen traffic, which is why it never carried the user-image routes. So finding the next one of these needs more recorded traffic — the profile page, the library dialog, the picker — rather than a scan of what is in the tree. That is a scenario-authoring job, and it is the shape `bundle.json` already has.
Sign in to join this conversation.
No description provided.