fix(attachment): resolve string X-Hero-User in /api/attachment/{id} #29
No reviewers
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_collab!29
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/attachment-http-auth-resolve-external-id"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #28.
Closes the canvas image upload regression in proxy mode.
GET /api/attachment/{id}returned 401 for every authenticated user because the HTTP byte-streaming handler parsedX-Hero-Userasu64directly — that worked for the legacy dev-mode picker convention (numericcaller_id) but silently failed in proxy mode where the proxy injectsexternal_id/email strings.Symptom
User in proxy mode uploads an image to a canvas. The upload RPC succeeds, the editor inserts an
<img>tag pointing at/api/attachment/{id}, the browser fetches that URL, and gets 401 Unauthorized → broken-image placeholder.DevTools console:
Affects every proxy-mode user, including the image's own uploader. Pre-existing bug — surfaced during dogfooding in #10's testing thread.
Root cause
crates/hero_collab_server/src/handlers/attachment_http.rs:79did:In proxy mode,
X-Hero-User: viewer_test@example.com.parse::<u64>()returnsNone→caller_id=None→check_attachment_accesshits the proxy-mode fail-closed branch → 401. The JSON-RPC dispatcher (main.rs::handle_rpc) does the right thing (tryexternal_id, thenemail/alias, with acaller_id_cachehot path), but the HTTP attachment route never got the same treatment.Fix
Mirror the dispatcher's two-step resolution:
parse::<u64>(preserves legacy dev-mode-numeric path).state.caller_id_cache(populated by every authenticated RPC, so a user who's already loaded the canvas page has a hit here).external_idfirst, thenemail/aliasfallback.DB-error handling is deliberately permissive (mapped to
None) instead of the dispatcher's noisyInternal-error path. A transient lock contention here would otherwise look like permission-denied to the browser, which is a worse failure mode than transparent re-resolution. The dispatcher logs noisily on the same code path, so blame is recoverable from logs even if the HTTP path swallows the error code.Test plan
GET /api/attachment/1withX-Hero-User: viewer_test@example.com→ 200 OK + image bytes (was 401).GET /api/attachment/1withX-Hero-User: 2(numeric) → still 200 OK (legacy dev-mode preserved).GET /api/attachment/1with no header → 401 (proxy-mode fail-closed semantic preserved).GET /api/attachment/1withX-Hero-User: nonexistent-user@example.com→ 401 (no DB row → caller_id=None).attachment.get— that path was already correct and is unchanged.cargo build --release -p hero_collab_serverclean.Out of scope
attachment_http.rskeeps this PR scoped to the actual bug; if a third HTTP route ever needs the same logic, that's the trigger to extract.🤖 Generated with Claude Code
The HTTP byte-streaming route for attachments parsed `X-Hero-User` as `u64` directly. That worked in the legacy dev-mode picker convention (numeric `caller_id`) but silently failed in proxy mode, where the proxy injects `external_id`/email strings — `parse::<u64>()` returned None, `caller_id=None` reached `check_attachment_access`, and the fail-closed proxy-mode branch returned 401 for every image GET. Symptom: every canvas image upload in proxy mode rendered as a broken placeholder, with `GET /api/attachment/{id}` returning 401 in DevTools even for the image's own uploader. Fix: mirror the JSON-RPC dispatcher's two-step resolution. Try `parse::<u64>` first (preserves the dev-mode-numeric path), then fall back to `state.caller_id_cache` (populated by every authenticated RPC call), then to a DB lookup on `external_id` and finally on `email`/`alias`. Cache is populated on hit so subsequent attachment fetches skip the DB. Cache- and lock-failures are non-fatal — they just trigger re-resolution next time. DB-error handling is deliberately permissive (mapped to None) rather than the dispatcher's noisy Internal-error path: a transient lock contention here would otherwise look like permission-denied to the browser, which is a worse failure mode than transparent re-resolution. The dispatcher logs noisily on the same path, so blame is recoverable from logs even if attachment GETs swallow the error code. Verified in proxy mode: `GET /api/attachment/1` with `X-Hero-User: viewer_test@example.com` returns 200 + image bytes (was 401). HTTP and JSON-RPC paths now agree on identity resolution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>