fix(web): guard empty canvas blob + fall back to original file on upload (BUNYIP-408) #402

Merged
longjacksonle merged 1 commit from fix/BUNYIP-408-avatar-empty-blob into main 2026-07-28 23:30:11 +02:00

Real root cause

The upload error is the bunyip-web message "The selected file is empty." (read_avatar_upload when bytes.is_empty()). So the upload never reached bunyip-api - the client was sending a 0-byte file. My earlier stale-API theory was wrong; this is a client bug.

Why: the re-encode used canvas.toBlob('image/webp', ...), and some browsers return an empty (0-byte) Blob there instead of null. An empty Blob is truthy, so the old if(!blob) guard let it through, and the empty part uploaded → 400.

Fix (bulletproof)

  • Encode to JPEG (landed in #401) instead of WebP - reliable canvas support.
  • Treat blob.size === 0 (or a null blob, or any processing error) as "re-encode failed".
  • On that failure, upload the original validated file instead. The API accepts png/jpeg/webp/gif and re-validates, so the user's pick still uploads even if canvas re-encoding is unavailable - only the client-side downscale is skipped in that fallback path.
  • upload() also refuses a zero-size payload outright with a clear inline error, so an empty request can never round-trip again.

Verification

  • JS validated with node --check.
  • just check-container green: fmt + clippy -D warnings + all test binaries (116 web tests).

Follow-up to #401 (data:-URL preview) and #400 (inline CSS). With this, the change-photo flow uploads real bytes on every browser.

🤖 Generated with Claude Code

## Real root cause The upload error is the bunyip-web message **"The selected file is empty."** (`read_avatar_upload` when `bytes.is_empty()`). So the upload never reached bunyip-api - the **client was sending a 0-byte file**. My earlier stale-API theory was wrong; this is a client bug. Why: the re-encode used `canvas.toBlob('image/webp', ...)`, and some browsers return an **empty (0-byte) Blob** there instead of `null`. An empty Blob is truthy, so the old `if(!blob)` guard let it through, and the empty part uploaded → 400. ## Fix (bulletproof) - Encode to **JPEG** (landed in #401) instead of WebP - reliable canvas support. - Treat `blob.size === 0` (or a null blob, or any processing error) as "re-encode failed". - On that failure, **upload the original validated file** instead. The API accepts png/jpeg/webp/gif and re-validates, so the user's pick still uploads even if canvas re-encoding is unavailable - only the client-side downscale is skipped in that fallback path. - `upload()` also refuses a zero-size payload outright with a clear inline error, so an empty request can never round-trip again. ## Verification - JS validated with `node --check`. - `just check-container` green: fmt + clippy `-D warnings` + all test binaries (116 web tests). Follow-up to #401 (data:-URL preview) and #400 (inline CSS). With this, the change-photo flow uploads real bytes on every browser. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(web): guard empty canvas blob + fall back to original file on upload (BUNYIP-408)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 55s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m8s
Create release / Create release from merged PR (pull_request) Has been skipped
27a7b08d18
Real cause of the upload 400, from the actual server message "The selected file is empty": the client was uploading a 0-byte file. The re-encode used `canvas.toBlob('image/webp')`, and some browsers return an EMPTY (0-byte) Blob there rather than null. An empty Blob is truthy, so the old `if(!blob)` guard passed it straight through and the empty part uploaded, which bunyip-web's `read_avatar_upload` rejects as empty (400). This is a client bug, not the stale-API theory from the previous commit - the upload never reached the API.

Two guards make it bulletproof:
- Encode to JPEG (already in this branch) instead of WebP - reliable canvas support - and treat `blob.size === 0` (or a null blob, or any processing error) as "re-encode failed".
- On that failure, upload the ORIGINAL validated file instead. The API accepts png/jpeg/webp/gif and re-validates, so the user's pick still uploads even if canvas re-encoding is unavailable; only the client-side downscale is skipped in that path.
- `upload()` also refuses a zero-size payload outright with a clear inline error rather than round-tripping an empty request.

JS validated with `node --check`; `just check-container` green (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:26:41 +02:00
longjacksonle deleted branch fix/BUNYIP-408-avatar-empty-blob 2026-07-28 23:30:11 +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!402
No description provided.