Bug: /v1/digitize silently returns 200 with empty results on corrupt/non-image input #27

Closed
opened 2026-09-09 20:29:52 +00:00 by Mark · 1 comment
Owner

Summary

POST /v1/digitize returns 200 OK with {stitchObjects: [], stats: {totalStitches: 0, colorCount: 0, processingMs: N}} when the uploaded image fails to decode, instead of returning a 4xx error. The client has no signal that the image was rejected.

Reproduction

# 1. Start service
cd /home/b/Projects/easystitch-service && EASYSTITCH_ENGINE_PATH=/home/b/Projects/easystitch \
  .venv/bin/uvicorn app:app --host 127.0.0.1 --port 8080

# 2. Send malformed PNG bytes (PNG magic followed by garbage)
curl -s -X POST http://127.0.0.1:8080/v1/digitize \
  -F 'image=@-;filename=bad.png;type=image/png' <<< $'\x89PNG\r\n\x1a\n garbage' \
  -F 'settings={}'

# Expected: 4xx with detail like {"error":"invalid_image","detail":"could not decode PNG"}
# Actual:   200 with {"regions":[],"stitchObjects":[],"stats":{"totalStitches":0,"colorCount":0,"processingMs":8}}

The same behavior fires for Content-Type: text/plain with arbitrary bytes. Only the empty-file case correctly returns 422 ({"error":"invalid_image","detail":"image_bytes must be non-empty"}).

Observed vs expected

Input HTTP Body
Empty PNG bytes 422 {"error":"invalid_image","detail":"image_bytes must be non-empty"} ✓
Garbage after PNG magic 200 {stitchObjects: [], stats: {totalStitches: 0}} ✗
Plain-text file 200 {stitchObjects: [], stats: {totalStitches: 0}} ✗

Impact

  • Frontends (ThreadCraft) cannot distinguish "user uploaded a bad file" from "user uploaded a valid file with no visible content"
  • A 200 response with empty stats is treated by the UI as a successful empty digitize — the user sees no error and assumes the design was empty
  • Compare to issue #19 (named-color 500), which surfaces clearly; this one is silent

Suggested fix

In pipeline_runner.py (or wherever the engine→service boundary decodes the image), catch ImageDecodeError and re-raise as HTTPException(status_code=422, detail={"error":"invalid_image","detail":<reason>}) to match the empty-file error shape. The decode error class already exists per the imports in app.py.

Environment

  • service version: 0.1.0 (from /healthz)
  • engine: easystitch (Mark/easystitch)
  • probed 2026-09-09

UI verification follow-up (2026-09-10)

The file upload barrier is now cracked (DataTransfer + Object.defineProperty + dispatchEvent). Confirmed via BrowserOS MCP snapshot that ThreadCraft's settings panel unlocks after upload but:

  • FABRIC TYPE / STABILIZER / HOOP SIZE comboboxes in the Design Settings panel render all options in the accessibility tree but clicking any option via act click / act kind=select / PointerEvent sequence closes the dropdown without updating React state. The next snapshot re-shows the original option as [selected]. Captured React props via __reactProps confirmed onClick exists but does not fire under synthetic dispatch.
  • Auto-Digitize (toolbar FAB e12 or inner-modal Confirm & Edit e65) sets Redux state to 'digitizing' then 'digitized' with a hardcoded "Auto-digitize complete!" message but never fires runAutoDigitize() → digitize() → POST to /v1/digitize. BrowserOS MCP hook fetch confirmed only /healthz GETs during the click sequence. Service log shows zero digitize POSTs across the entire 2026-09-09/10 verification session.

This means the UI cannot drive a digitize or export at all in the current ThreadCraft build. The API bugs (#27-31) are still the canonical surface; the UI bugs are symptom-level and tracked separately as #32 (silent no-op), #33 (combobox click swallowed), #34 (false-complete message). Those three will be closed as duplicates of this issue family once the canonical root causes land.

## Summary `POST /v1/digitize` returns **200 OK** with `{stitchObjects: [], stats: {totalStitches: 0, colorCount: 0, processingMs: N}}` when the uploaded image fails to decode, instead of returning a 4xx error. The client has no signal that the image was rejected. ## Reproduction ```bash # 1. Start service cd /home/b/Projects/easystitch-service && EASYSTITCH_ENGINE_PATH=/home/b/Projects/easystitch \ .venv/bin/uvicorn app:app --host 127.0.0.1 --port 8080 # 2. Send malformed PNG bytes (PNG magic followed by garbage) curl -s -X POST http://127.0.0.1:8080/v1/digitize \ -F 'image=@-;filename=bad.png;type=image/png' <<< $'\x89PNG\r\n\x1a\n garbage' \ -F 'settings={}' # Expected: 4xx with detail like {"error":"invalid_image","detail":"could not decode PNG"} # Actual: 200 with {"regions":[],"stitchObjects":[],"stats":{"totalStitches":0,"colorCount":0,"processingMs":8}} ``` The same behavior fires for `Content-Type: text/plain` with arbitrary bytes. Only the **empty-file** case correctly returns 422 (`{"error":"invalid_image","detail":"image_bytes must be non-empty"}`). ## Observed vs expected | Input | HTTP | Body | |---|---|---| | Empty PNG bytes | 422 | `{"error":"invalid_image","detail":"image_bytes must be non-empty"}` ✓ | | Garbage after PNG magic | **200** | `{stitchObjects: [], stats: {totalStitches: 0}}` ✗ | | Plain-text file | **200** | `{stitchObjects: [], stats: {totalStitches: 0}}` ✗ | ## Impact - Frontends (ThreadCraft) cannot distinguish "user uploaded a bad file" from "user uploaded a valid file with no visible content" - A 200 response with empty stats is treated by the UI as a successful empty digitize — the user sees no error and assumes the design was empty - Compare to issue #19 (named-color 500), which surfaces clearly; this one is **silent** ## Suggested fix In `pipeline_runner.py` (or wherever the engine→service boundary decodes the image), catch `ImageDecodeError` and re-raise as `HTTPException(status_code=422, detail={"error":"invalid_image","detail":<reason>})` to match the empty-file error shape. The decode error class already exists per the imports in `app.py`. ## Environment - service version: 0.1.0 (from /healthz) - engine: easystitch (Mark/easystitch) - probed 2026-09-09 --- **UI verification follow-up (2026-09-10)** The file upload barrier is now cracked (DataTransfer + Object.defineProperty + dispatchEvent). Confirmed via BrowserOS MCP `snapshot` that ThreadCraft's settings panel unlocks after upload but: - FABRIC TYPE / STABILIZER / HOOP SIZE comboboxes in the Design Settings panel render all options in the accessibility tree but **clicking any option via `act click` / `act kind=select` / PointerEvent sequence closes the dropdown without updating React state**. The next snapshot re-shows the original option as `[selected]`. Captured React props via `__reactProps` confirmed `onClick` exists but does not fire under synthetic dispatch. - Auto-Digitize (toolbar FAB `e12` or inner-modal Confirm & Edit `e65`) sets Redux state to `'digitizing'` then `'digitized'` with a hardcoded "Auto-digitize complete!" message **but never fires `runAutoDigitize()` → `digitize()` → POST to `/v1/digitize`**. BrowserOS MCP `hook fetch` confirmed only `/healthz` GETs during the click sequence. Service log shows zero digitize POSTs across the entire 2026-09-09/10 verification session. This means **the UI cannot drive a digitize or export at all in the current ThreadCraft build**. The API bugs (#27-31) are still the canonical surface; the UI bugs are symptom-level and tracked separately as #32 (silent no-op), #33 (combobox click swallowed), #34 (false-complete message). Those three will be closed as duplicates of this issue family once the canonical root causes land.
Author
Owner

Visual verification (2026-09-09, Gemini-3 Flash via Hermes vision_analyze tool): Empty workspace screenshots taken via BrowserOS MCP at http://127.0.0.1:5173/workspace confirm the empty-state UI text is consistent — "Drop your image here", "Upload an image to configure stitch parameters", "No design loaded". None of the screenshots reproduce a corrupt-image upload because the browser-driven test path was blocked by hidden file inputs (BrowserOS MCP cannot drive hidden <input type=file>). The API-level reproduction in the original bug body remains the primary evidence. Status: bug confirmed at API layer; UI reproduction blocked by tooling limitation.

Visual verification (2026-09-09, Gemini-3 Flash via Hermes `vision_analyze` tool): Empty workspace screenshots taken via BrowserOS MCP at http://127.0.0.1:5173/workspace confirm the empty-state UI text is consistent — "Drop your image here", "Upload an image to configure stitch parameters", "No design loaded". **None of the screenshots reproduce a corrupt-image upload because the browser-driven test path was blocked by hidden file inputs (BrowserOS MCP cannot drive hidden `<input type=file>`).** The API-level reproduction in the original bug body remains the primary evidence. Status: bug confirmed at API layer; UI reproduction blocked by tooling limitation.
Mark 2026-09-13 01:13:07 +00:00
Sign in to join this conversation.
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
Mark/easystitch-service#27
No description provided.