A tab icon you can actually see, and photo uploads that accept a photo #14

Merged
austin merged 1 commits from favicon-and-uploads into main 2026-07-27 19:06:36 -05:00
Owner

Draft. Two unrelated things, both reported together.

Why uploads failed

The cause was an absence. Nothing configured spring.servlet.multipart, so Boot's default max-file-size of 1 MB applied — and a photo off a phone is 3–12 MB. The container rejected every one before it reached ProductPhotoService, the class whose entire job is turning "whatever came off a phone" into a resized webp. The resizing pipeline could never run on the input it was written for.

From the running container's log, repeatedly:

FileSizeLimitExceededException: The field photos exceeds its maximum permitted size of 1048576 bytes

Nothing else was involved — Caddy proxies with no body limit, so Spring's default was the only constraint.

Fix: 15 MB a file, 60 MB a request (the file input is multiple, so one submit can carry several).

The failure was also ugly, which is a separate bug

Parsed eagerly, an over-sized part throws from inside Tomcat's parameter parsing, where no @ExceptionHandler can reach it. So the request died as a 500 and then died a second time forwarding to /error, because that forward re-parsed the same too-large request — the paired Exception Processing [ErrorPage...] lines in the log. resolve-lazily: true moves the throw into argument binding, where AdminController catches it and returns the same problem flash the domain's other refusals use. server.tomcat.max-swallow-size: -1 lets the rest of the body be discarded so the browser actually receives that redirect instead of a connection reset.

The favicon was not missing, it was unusable

rel=icon pointed at the 1000×1000 logo PNGs — 71 KB fetched to paint 16 square pixels, of a vine branch drawn in hairlines whose strokes are thinner than a pixel at that size. It arrived as a grey smudge.

Replaced with a real icon set built from one leaf of that branch, filled rather than stroked, because at 16px a silhouette survives and an outline does not. I drew a midrib first and it cut the leaf into two pale slivers at tab size, so it went — the tilt, the two points and the stem carry the shape without it. The SVG answers prefers-color-scheme itself, which a .ico cannot, so a dark tab strip gets sage on bakery-900 rather than a glowing cream tile.

The .ico is listed first deliberately: a browser takes the last format it understands, so reversing the two hands Chrome the bitmap.

file size for
favicon.svg 2.0 KB every modern browser, theme-aware
favicon.ico 15 KB 16/32/48, Safari and older
apple-touch-icon.png 4.9 KB 180×180, iOS home screen

Tests

68 passing, 0 failures (was 64). Four new:

  • the multipart numbers are asserted rather than trusted, including an explicit isNotEqualTo(1MB) — a default that was never set is exactly what comes back silently
  • an over-sized photo is refused as a readable sentence, not a stack trace
  • the icon links are present, in the right order, and the old 1000×1000 ones are gone
  • all three icon files actually return 200 (a link to a 404 is worse than no link)

What is NOT verified

No real photo has been uploaded through this. MockMvc does not enforce the container's multipart limits, so there is no way to provoke the real parse failure in a test — the refusal test stages the throw from the service instead, which covers the handler wiring but not the container. The everyday path (a genuine 4 MB JPEG going in, getting resized, landing in MinIO) needs one upload through /admin, and that needs an Authentik login I do not have.

Worth one real upload before merging.

🤖 Generated with Claude Code

Draft. Two unrelated things, both reported together. ## Why uploads failed The cause was an **absence**. Nothing configured `spring.servlet.multipart`, so Boot's default `max-file-size` of **1 MB** applied — and a photo off a phone is 3–12 MB. The container rejected every one before it reached `ProductPhotoService`, the class whose entire job is turning *"whatever came off a phone"* into a resized webp. The resizing pipeline could never run on the input it was written for. From the running container's log, repeatedly: ``` FileSizeLimitExceededException: The field photos exceeds its maximum permitted size of 1048576 bytes ``` Nothing else was involved — Caddy proxies with no body limit, so Spring's default was the only constraint. **Fix:** 15 MB a file, 60 MB a request (the file input is `multiple`, so one submit can carry several). ### The failure was also ugly, which is a separate bug Parsed eagerly, an over-sized part throws from inside Tomcat's parameter parsing, where no `@ExceptionHandler` can reach it. So the request died as a 500 and then **died a second time forwarding to `/error`**, because that forward re-parsed the same too-large request — the paired `Exception Processing [ErrorPage...]` lines in the log. `resolve-lazily: true` moves the throw into argument binding, where `AdminController` catches it and returns the same `problem` flash the domain's other refusals use. `server.tomcat.max-swallow-size: -1` lets the rest of the body be discarded so the browser actually receives that redirect instead of a connection reset. ## The favicon was not missing, it was unusable `rel=icon` pointed at the **1000×1000 logo PNGs** — 71 KB fetched to paint 16 square pixels, of a vine branch drawn in hairlines whose strokes are thinner than a pixel at that size. It arrived as a grey smudge. Replaced with a real icon set built from **one leaf** of that branch, filled rather than stroked, because at 16px a silhouette survives and an outline does not. I drew a midrib first and it cut the leaf into two pale slivers at tab size, so it went — the tilt, the two points and the stem carry the shape without it. The SVG answers `prefers-color-scheme` itself, which a `.ico` cannot, so a dark tab strip gets sage on bakery-900 rather than a glowing cream tile. The `.ico` is listed **first** deliberately: a browser takes the last format it understands, so reversing the two hands Chrome the bitmap. | file | size | for | |---|---|---| | `favicon.svg` | 2.0 KB | every modern browser, theme-aware | | `favicon.ico` | 15 KB | 16/32/48, Safari and older | | `apple-touch-icon.png` | 4.9 KB | 180×180, iOS home screen | ## Tests **68 passing, 0 failures** (was 64). Four new: - the multipart numbers are **asserted rather than trusted**, including an explicit `isNotEqualTo(1MB)` — a default that was never set is exactly what comes back silently - an over-sized photo is refused as a readable sentence, not a stack trace - the icon links are present, in the right order, and the old 1000×1000 ones are gone - all three icon files actually return 200 (a link to a 404 is worse than no link) ## What is NOT verified **No real photo has been uploaded through this.** MockMvc does not enforce the container's multipart limits, so there is no way to provoke the real parse failure in a test — the refusal test stages the throw from the service instead, which covers the handler wiring but not the container. The everyday path (a genuine 4 MB JPEG going in, getting resized, landing in MinIO) needs one upload through `/admin`, and that needs an Authentik login I do not have. **Worth one real upload before merging.** 🤖 Generated with [Claude Code](https://claude.com/claude-code)
austin added 1 commit 2026-07-27 18:46:15 -05:00
A tab icon you can actually see, and photo uploads that accept a photo
build-and-publish / build (pull_request) Successful in 2m14s
537273cb62
Two unrelated things the bakery hit on the same afternoon.

THE FAVICON was not missing, it was unusable. The head pointed rel=icon at the 1000x1000 logo
PNGs, so a browser fetched 71 KB to paint 16 square pixels, and the mark is a vine branch drawn in
hairlines -- strokes thinner than one pixel at that size -- which arrives as a grey smudge. Replaced
with a real icon set built from one leaf of that branch, filled rather than stroked, because at 16px
a silhouette survives and an outline does not. A midrib was drawn first and cut the leaf into two
pale slivers at tab size, so it went; the tilt, the two points and the stem carry the shape. The SVG
answers prefers-color-scheme itself, which a .ico cannot, so the dark tab strip gets sage on
bakery-900 instead of a glowing cream tile. The .ico is listed first on purpose: a browser takes the
last format it understands, so reversing the two would hand Chrome the bitmap.

PHOTO UPLOADS failed on anything over 1 MB, which is every photo a phone takes. The cause was an
absence: nothing configured spring.servlet.multipart, so Boot's 1 MB default applied and the
container rejected the file with FileSizeLimitExceededException before it reached
ProductPhotoService -- the class whose entire job is turning "whatever came off a phone" into a
resized webp. The pipeline could never run on the input it was written for. Now 15 MB a file and
60 MB a request, the latter because the file input is `multiple`.

The failure was also ugly, and that is fixed separately: parsed eagerly, an over-sized part throws
from inside Tomcat's parameter parsing where no @ExceptionHandler can reach it, so the request died
as a 500 and then died again forwarding to /error, because that forward re-parsed the same too-large
request (the paired "Exception Processing [ErrorPage...]" lines in the log). resolve-lazily moves the
throw into argument binding, where AdminController now catches it and returns the same `problem`
flash the domain's other refusals use. max-swallow-size lets the body be discarded so the browser
receives that redirect rather than a connection reset.

The multipart numbers are asserted rather than trusted, because a default that was never set is
exactly the kind of thing that comes back silently.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
austin marked the pull request as ready for review 2026-07-27 19:06:26 -05:00
austin merged commit 6b057db191 into main 2026-07-27 19:06:36 -05:00
austin deleted branch favicon-and-uploads 2026-07-27 19:06:36 -05:00
This repo is archived. You cannot comment on pull requests.
No Reviewers
No labels
1 Participants
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: TheVine/itsthevine#14