fix: compress images in the browser so uploads clear the 1 MB ingress cap #4

Merged
iec-admin merged 1 commit from claw/upload-compress-b7c2 into main 2026-09-11 22:58:15 +00:00
Contributor

Closes #3.

Uploads over 1 MB failed with a bare 413. The cause is the platform ingress, which refuses bodies above 1 MB before Next.js is reached — so the 10 MB / 15 MB limits the app advertised could never be honoured, and no handler ran that could explain the failure.

Approach

Re-encode photos in the browser, the only point where the limit can be met without a platform change. New src/lib/media/compress.ts: downscale to a 2560px longest edge, re-encode to WebP (JPEG fallback), stepping quality down and then dimensions until the result fits a 900 KB budget — under the cap with headroom for multipart overhead.

Files already under budget and within the dimension limit pass through untouched, so nothing is needlessly degraded.

Validation split by what it judges: validateSourceFile on what was picked, validateMediaFile on what is actually sent.

Verified against a real browser

Driven through the admin UI with Playwright on a local dev server:

Case Result
12 MB, 4032×3024 incompressible noise (worst case) uploaded — 899,976 bytes, 1440×1080
2.4 MB realistic photograph uploaded — 364,972 bytes, 2560×1920 (full resolution kept)
278 KB photo already within limits passed through byte-identical, name unchanged
2.9 MB video blocked client-side, no request sent, clear message

The video case now reads: "Video is 2.9 MB — video files cannot be compressed in the browser and the upload limit is 0.9 MB. Compress it before uploading, or host it externally and link to it."

npm run type-check and npm run build both clean.

Known limitation

Video and animated GIF can't be re-encoded in a canvas, so they're still bound by the 1 MB ingress cap — they now fail fast with an explanation rather than a raw 413. The real fix for those is nginx.ingress.kubernetes.io/proxy-body-size on the ingress; no catalog-info field exposes it today.

🤖 Generated with Claude Code

Closes #3. Uploads over 1 MB failed with a bare `413`. The cause is the platform ingress, which refuses bodies above 1 MB before Next.js is reached — so the 10 MB / 15 MB limits the app advertised could never be honoured, and no handler ran that could explain the failure. ## Approach Re-encode photos in the browser, the only point where the limit can be met without a platform change. New `src/lib/media/compress.ts`: downscale to a 2560px longest edge, re-encode to WebP (JPEG fallback), stepping quality down and then dimensions until the result fits a 900 KB budget — under the cap with headroom for multipart overhead. Files already under budget and within the dimension limit pass through untouched, so nothing is needlessly degraded. Validation split by what it judges: `validateSourceFile` on what was picked, `validateMediaFile` on what is actually sent. ## Verified against a real browser Driven through the admin UI with Playwright on a local dev server: | Case | Result | |---|---| | 12 MB, 4032×3024 incompressible noise (worst case) | uploaded — 899,976 bytes, 1440×1080 | | 2.4 MB realistic photograph | uploaded — 364,972 bytes, **2560×1920** (full resolution kept) | | 278 KB photo already within limits | passed through **byte-identical**, name unchanged | | 2.9 MB video | blocked client-side, no request sent, clear message | The video case now reads: *"Video is 2.9 MB — video files cannot be compressed in the browser and the upload limit is 0.9 MB. Compress it before uploading, or host it externally and link to it."* `npm run type-check` and `npm run build` both clean. ## Known limitation Video and animated GIF can't be re-encoded in a canvas, so they're still bound by the 1 MB ingress cap — they now fail fast with an explanation rather than a raw 413. The real fix for those is `nginx.ingress.kubernetes.io/proxy-body-size` on the ingress; no catalog-info field exposes it today. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Uploading anything over 1 MB failed with a bare 413. The cause was not our
code: the platform ingress refuses request bodies above 1 MB before Next.js
is reached, so the 10 MB image / 15 MB video limits the app advertised could
never be honoured, and no handler ran that could explain the failure.

Re-encode photos before sending them, which is the only point at which the
limit can be met without a platform change. Images are downscaled to a 2560px
longest edge and re-encoded to WebP (JPEG where the canvas cannot encode
WebP) stepping quality down until they fit a 900 KB budget — under the cap
with room for the multipart overhead that rides along in the same body. A
file already under budget and within the dimension limit is passed through
untouched rather than needlessly re-encoded.

This is worth doing on its own merits: every image is served through
next/image, which re-encodes on demand anyway, so keeping the untouched
original only cost database space and hero re-encode time on a small pod.

Video and animated GIF cannot be re-encoded in a canvas, so they remain bound
by the ingress cap and now fail fast with an explanation instead of a 413.
Split validation accordingly: validateSourceFile judges what the person
picked, validateMediaFile judges what is actually sent.

close #3

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
iec-admin deleted branch claw/upload-compress-b7c2 2026-09-11 22:58:15 +00: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
martin/ballantyne-www!4
No description provided.