fix(web): avatar preview uses a data: URL, not a CSP-blocked blob: URL (BUNYIP-408) #401

Merged
longjacksonle merged 1 commit from fix/BUNYIP-408-avatar-preview-csp into main 2026-07-28 23:06:43 +02:00

Symptoms

"Change photo doesn't work", plus in the console:

  • Loading the image 'blob:https://a8n.systems/...' violates the following Content Security Policy directive: "img-src 'self' data: https:"
  • /settings/avatar:1 Failed to load resource: the server responded with a status of 400

Two separate issues

1. CSP-blocked preview - real code bug, fixed here

The instant local preview used URL.createObjectURL(file), which yields a blob: URL. The page CSP is img-src 'self' data: https: (no blob:), so the preview image was refused. Fixed by previewing with a data: URL (FileReader.readAsDataURL), which the existing CSP already allows - no CSP change required.

2. The 400 on /settings/avatar - a stale bunyip-api deployment, not a code bug

settings_avatar (BFF) returns 400 BAD_REQUEST whenever its upstream call to POST /v1/users/me/avatar errors. A bunyip-api image that predates the BUNYIP-408 endpoints returns 404 there - which is the "404" originally reported, surfaced to the browser as a 400.

I verified the code is correct, so this is a deploy issue:

  • A routing test showed GET/POST/DELETE on /users/me/avatar all dispatch to their handlers (401 unauthenticated), never 404/405.
  • A real Chrome-canvas WebP (VP8X) and a JPEG both pass the API's exact validators (infer 0.16 magic-byte sniff + imagesize 0.13 dimension parse). So neither the routes nor the image format is at fault.

Recent PRs (#398-#400) were bunyip-web only. If the deploy pipeline only rebuilt bunyip-web, bunyip-api is still on a pre-BUNYIP-408 image. Redeploying bunyip-api restores the upload endpoint.

Also (refinement, not the fix)

Switched the client re-encode from WebP to JPEG with a white backing. Both formats are accepted by the API; JPEG keeps the stored avatar small and flattens transparency to white instead of black.

Verification

  • Root cause confirmed empirically (canvas WebP + JPEG both validate server-side; routes dispatch).
  • just check-container green: fmt + clippy -D warnings + all test binaries (116 web tests).

🤖 Generated with Claude Code

## Symptoms "Change photo doesn't work", plus in the console: - `Loading the image 'blob:https://a8n.systems/...' violates the following Content Security Policy directive: "img-src 'self' data: https:"` - `/settings/avatar:1 Failed to load resource: the server responded with a status of 400` ## Two separate issues ### 1. CSP-blocked preview - real code bug, fixed here The instant local preview used `URL.createObjectURL(file)`, which yields a `blob:` URL. The page CSP is `img-src 'self' data: https:` (no `blob:`), so the preview image was refused. Fixed by previewing with a `data:` URL (`FileReader.readAsDataURL`), which the existing CSP already allows - no CSP change required. ### 2. The 400 on /settings/avatar - a stale bunyip-api deployment, not a code bug `settings_avatar` (BFF) returns `400 BAD_REQUEST` whenever its upstream call to `POST /v1/users/me/avatar` errors. A bunyip-api image that predates the BUNYIP-408 endpoints returns **404** there - which is the "404" originally reported, surfaced to the browser as a 400. I verified the code is correct, so this is a deploy issue: - A routing test showed `GET`/`POST`/`DELETE` on `/users/me/avatar` all dispatch to their handlers (401 unauthenticated), never 404/405. - A real Chrome-canvas **WebP** (VP8X) **and** a **JPEG** both pass the API's exact validators (`infer` 0.16 magic-byte sniff + `imagesize` 0.13 dimension parse). So neither the routes nor the image format is at fault. **Recent PRs (#398-#400) were bunyip-web only.** If the deploy pipeline only rebuilt bunyip-web, bunyip-api is still on a pre-BUNYIP-408 image. **Redeploying bunyip-api restores the upload endpoint.** ## Also (refinement, not the fix) Switched the client re-encode from WebP to JPEG with a white backing. Both formats are accepted by the API; JPEG keeps the stored avatar small and flattens transparency to white instead of black. ## Verification - Root cause confirmed empirically (canvas WebP + JPEG both validate server-side; routes dispatch). - `just check-container` green: fmt + clippy `-D warnings` + all test binaries (116 web tests). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(web): avatar preview uses a data: URL, not a CSP-blocked blob: URL (BUNYIP-408)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 50s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m7s
Create release / Create release from merged PR (pull_request) Has been skipped
5763c82772
Reported: "change photo doesn't work", a CSP violation, and a 400 on /settings/avatar.

Two separate things, one of them a real code bug:

1. CSP-blocked preview (fixed here). The instant local preview used `URL.createObjectURL(file)`, which is a `blob:` URL. The page CSP is `img-src 'self' data: https:` - no `blob:` - so the browser refused to render the preview ("Loading the image 'blob:...' violates ... img-src"). Fixed by previewing with a `data:` URL via `FileReader.readAsDataURL` (allowed by the existing CSP), so no CSP change is needed.

2. The 400 on /settings/avatar is NOT a code bug - it is a stale bunyip-api deployment. `settings_avatar` returns BAD_REQUEST whenever the upstream `POST /v1/users/me/avatar` call errors, and a bunyip-api image that predates the BUNYIP-408 endpoints returns 404 there (the "404" originally reported). Confirmed the code is correct: a routing test showed GET/POST/DELETE on `/users/me/avatar` all dispatch (401 unauth, never 404/405), and running a real Chrome-canvas WebP AND JPEG through the API's exact validators (infer 0.16 + imagesize 0.13) both pass - so neither the routes nor the image format is at fault. The recent PRs (#398-#400) were bunyip-web only; redeploying bunyip-api restores the endpoint.

Also switched the client re-encode from WebP to JPEG with a white backing: both are accepted by the API, but JPEG keeps the stored avatar small and flattens transparency to white instead of black. This is a refinement, not the fix - the WebP validated fine.

Verified: `just check-container` green (fmt + clippy -D warnings + all test binaries, 116 web tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5dcYueNHByRnWJDYoDX1W
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-07-28 23:04:43 +02:00
longjacksonle deleted branch fix/BUNYIP-408-avatar-preview-csp 2026-07-28 23:06:43 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!401
No description provided.