A damaged image is refused in words, not with a 500 (#1065) #1066

Merged
claude-bot merged 2 commits from fix/map-upload-500 into main 2026-10-04 04:30:04 +00:00
Collaborator

Closes #1065.

Summary

An image whose header reads but whose pixel data is broken answered 500 {"detail":"Internal server error"}. image_service.validate_and_reencode guarded only Image.open and verify(); the pixels are decoded later, in the re-encode, which was outside the try. The re-encode is now inside the guard and raises ImageError("This image is damaged or incomplete, so it could not be read."), which every caller (map images, page pictures, attachments, imports) already turns into a 422.

Found while reproducing a map-upload 500 in the journeys stack. A sound image uploads and tiles correctly there; of 23 variants tried, only the three damaged ones gave a 500.

Changelog

  • changelog.d/damaged-image-upload-500.fixed.md

Test plan

  • Failing first: at the test-only commit, 6 failed with the same OSError as the live traceback.
  • With the fix: the new test_damaged_image_upload.py, a route test in tests/wiki/test_maps_api.py (422, no image kept, tiles_status "none"), plus tests/wiki, tests/export and the enqueue tests: 865 passed.
  • ruff format and check clean; changelog and version checks OK.
  • Also seen in the running stack (journeys, main's images): a valid 800×600 PNG uploaded through nginx, tiled to ready and served its tile. A 4.1 MB PNG returned 200 through nginx, which confirms #1058's limit in a running build.
  • Not run: the full backend suite (CI), and this fix inside a rebuilt e2e image.

🤖 Generated with Claude Code

Closes #1065. ## Summary An image whose header reads but whose pixel data is broken answered `500 {"detail":"Internal server error"}`. `image_service.validate_and_reencode` guarded only `Image.open` and `verify()`; the pixels are decoded later, in the re-encode, which was outside the `try`. The re-encode is now inside the guard and raises `ImageError("This image is damaged or incomplete, so it could not be read.")`, which every caller (map images, page pictures, attachments, imports) already turns into a 422. Found while reproducing a map-upload 500 in the journeys stack. A sound image uploads and tiles correctly there; of 23 variants tried, only the three damaged ones gave a 500. ## Changelog - [x] `changelog.d/damaged-image-upload-500.fixed.md` ## Test plan - [x] **Failing first:** at the test-only commit, 6 failed with the same `OSError` as the live traceback. - [x] **With the fix:** the new `test_damaged_image_upload.py`, a route test in `tests/wiki/test_maps_api.py` (422, no image kept, `tiles_status` "none"), plus `tests/wiki`, `tests/export` and the enqueue tests: 865 passed. - [x] ruff format and check clean; changelog and version checks OK. - [x] **Also seen in the running stack** (journeys, main's images): a valid 800×600 PNG uploaded through nginx, tiled to `ready` and served its tile. A 4.1 MB PNG returned 200 through nginx, which confirms #1058's limit in a running build. - [ ] Not run: the full backend suite (CI), and this fix inside a rebuilt e2e image. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(webapp): a damaged image upload is refused with a 422 instead of a 500
All checks were successful
CI / Backend lint (ruff) (pull_request) Successful in 44s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m16s
CI / Does this change touch the recording path? (pull_request) Successful in 32s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot/backend version sync (pull_request) Successful in 37s
CI / Feedback site tests (pull_request) Successful in 1m3s
CI / Bot tests and audit (pull_request) Successful in 2m19s
CI / Docker image build (pull_request) Successful in 2m38s
CI / Frontend (shard 1/2) (pull_request) Successful in 11m6s
CI / Frontend (shard 2/2) (pull_request) Successful in 12m49s
CI / End-to-end journeys (phone) (pull_request) Successful in 13m59s
CI / End-to-end journeys (desktop) (pull_request) Successful in 15m20s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 19m10s
CI / Backend migration, tests, and audit (pull_request) Successful in 50m44s
9d8217ec5f
validate_and_reencode guarded only Image.open and verify(), which read the
header and (for a PNG) chunk checksums. The pixels are decoded later, in the
re-encode, which was outside the try: a JPEG cut short or a PNG with broken
image data raised OSError there and the route answered 500. The re-encode is
now inside the guard and raises ImageError, which every caller (map image,
attachment, page picture, import) already turns into a refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot deleted branch fix/map-upload-500 2026-10-04 04:30:05 +00:00
Sign in to join this conversation.
No description provided.