fix(images): fix iOS image upload and improve logging #80

Merged
addison merged 7 commits from exe-dev-bot/market:fix/ios-image-upload into main 2026-02-18 06:35:48 -05:00
Contributor

Problem

A user on iOS (iPhone, Safari 18.1.1) reported being unable to upload item images. The feature works on Linux and Android. Cloudflare logs showed minimal diagnostic information.

Root Cause Analysis

1. Critical: Race condition unmounts form during upload

The onSubmit flow called onSave(result.data, true) before uploadImages() completed. This triggered handleSave in ItemFormPage, which set success=true, causing React to unmount ItemForm and render the success message instead. The unmount cleanup fired abortControllerRef.current.abort(), killing all in-flight image uploads. The isUploading parameter was accepted in the type signature but completely ignored by handleSave.

This race existed on all platforms but iOS Safari is more aggressive about unmount timing, making the window tighter.

2. High: HEIC/HEIF not accepted

iOS cameras default to HEIC format. The file input accept attribute and ALLOWED_TYPES only included JPEG/PNG/WebP. While iOS Safari auto-converts HEIC on selection, the conversion is inconsistent across iOS versions — sometimes reporting empty file.type or image/heif, which validateImageFile rejected.

3. Medium: Web Workers in image compression

browser-image-compression with useWebWorker: true uses OffscreenCanvas and createImageBitmap inside workers. iOS Safari has had longstanding issues with these APIs in worker contexts.

4. Medium: No FileReader error handler

If FileReader.readAsDataURL failed (e.g., corrupted compression output), loadedCount never reached files.length, leaving the UI stuck in "Processing images..." state forever.

Changes

src/components/react/ItemForm.tsx

  • Fix race condition: Remove premature onSave(result.data, true) call. Now onSave is called only once, after uploads complete
  • HEIC/HEIF support: Add to file input accept attribute
  • Disable web workers: Set useWebWorker: false for iOS compatibility
  • FileReader error handler: Prevent stuck processing state
  • Preserve compression output type: Use compressedBlob.type instead of hardcoding image/jpeg
  • Diagnostic logging: Log file metadata on selection, compression failures with details, and abort events

src/lib/storage.ts

  • Magic byte validation: Replace MIME-type-only validation with binary header inspection. Detects JPEG, PNG, WebP, and HEIC/HEIF from file headers. Files with empty/generic MIME types (common iOS quirk) are accepted only if magic bytes confirm they are real images. This prevents attackers from uploading non-image files (SVG with JS, HTML) by spoofing MIME types.
  • HEIC/HEIF in allowed types: Add to ALLOWED_TYPES and MIME-to-extension map
  • Upload logging: Log file path, type, and size at upload start

tests/unit/storage.test.ts

  • Rewrite mock file helpers to use real magic byte headers
  • Add tests for HEIC/HEIF acceptance and extension mapping
  • Add tests for empty MIME type + valid magic bytes (iOS scenario)
  • Add tests for invalid magic bytes rejection
  • Add RIFF-without-WEBP rejection test

Testing

  • Upload image on desktop browser (regression check)
  • Upload image on iOS Safari (if available)
  • Upload HEIC photo from iOS camera roll
  • Verify success redirect waits for upload to complete
  • Check browser console for new diagnostic logging
  • just test-unit — all 331 tests pass
## Problem A user on iOS (iPhone, Safari 18.1.1) reported being unable to upload item images. The feature works on Linux and Android. Cloudflare logs showed minimal diagnostic information. ## Root Cause Analysis ### 1. Critical: Race condition unmounts form during upload The `onSubmit` flow called `onSave(result.data, true)` **before** `uploadImages()` completed. This triggered `handleSave` in `ItemFormPage`, which set `success=true`, causing React to unmount `ItemForm` and render the success message instead. The unmount cleanup fired `abortControllerRef.current.abort()`, killing all in-flight image uploads. The `isUploading` parameter was accepted in the type signature but completely ignored by `handleSave`. This race existed on all platforms but iOS Safari is more aggressive about unmount timing, making the window tighter. ### 2. High: HEIC/HEIF not accepted iOS cameras default to HEIC format. The file input `accept` attribute and `ALLOWED_TYPES` only included JPEG/PNG/WebP. While iOS Safari auto-converts HEIC on selection, the conversion is inconsistent across iOS versions — sometimes reporting empty `file.type` or `image/heif`, which `validateImageFile` rejected. ### 3. Medium: Web Workers in image compression `browser-image-compression` with `useWebWorker: true` uses `OffscreenCanvas` and `createImageBitmap` inside workers. iOS Safari has had longstanding issues with these APIs in worker contexts. ### 4. Medium: No FileReader error handler If `FileReader.readAsDataURL` failed (e.g., corrupted compression output), `loadedCount` never reached `files.length`, leaving the UI stuck in "Processing images..." state forever. ## Changes ### `src/components/react/ItemForm.tsx` - **Fix race condition**: Remove premature `onSave(result.data, true)` call. Now `onSave` is called only once, after uploads complete - **HEIC/HEIF support**: Add to file input `accept` attribute - **Disable web workers**: Set `useWebWorker: false` for iOS compatibility - **FileReader error handler**: Prevent stuck processing state - **Preserve compression output type**: Use `compressedBlob.type` instead of hardcoding `image/jpeg` - **Diagnostic logging**: Log file metadata on selection, compression failures with details, and abort events ### `src/lib/storage.ts` - **Magic byte validation**: Replace MIME-type-only validation with binary header inspection. Detects JPEG, PNG, WebP, and HEIC/HEIF from file headers. Files with empty/generic MIME types (common iOS quirk) are accepted only if magic bytes confirm they are real images. This prevents attackers from uploading non-image files (SVG with JS, HTML) by spoofing MIME types. - **HEIC/HEIF in allowed types**: Add to `ALLOWED_TYPES` and MIME-to-extension map - **Upload logging**: Log file path, type, and size at upload start ### `tests/unit/storage.test.ts` - Rewrite mock file helpers to use real magic byte headers - Add tests for HEIC/HEIF acceptance and extension mapping - Add tests for empty MIME type + valid magic bytes (iOS scenario) - Add tests for invalid magic bytes rejection - Add RIFF-without-WEBP rejection test ## Testing - [ ] Upload image on desktop browser (regression check) - [ ] Upload image on iOS Safari (if available) - [ ] Upload HEIC photo from iOS camera roll - [ ] Verify success redirect waits for upload to complete - [ ] Check browser console for new diagnostic logging - [ ] `just test-unit` — all 331 tests pass
- Fix critical race condition where onSave() unmounts form during upload,
  causing abort controller to kill in-flight image uploads. Now onSave is
  called only after uploads complete.
- Add HEIC/HEIF to accepted file types and MIME validation (iOS default)
- Handle empty/generic MIME types from iOS Safari auto-conversion
- Disable web workers for image compression (iOS Safari compatibility)
- Add FileReader error handler to prevent stuck 'processing' state
- Add diagnostic logging throughout upload pipeline for debugging
- Preserve actual output type from compression instead of hardcoding JPEG

Co-authored-by: Shelley <shelley@exe.dev>
test(storage): update tests for HEIC/HEIF and iOS MIME type handling
Some checks failed
CI / Lint, Type Check, Format & Unit Tests (pull_request) Failing after 1m12s
CI / E2E Tests (pull_request) Successful in 2m32s
f489b4a11c
Co-authored-by: Shelley <shelley@exe.dev>
fix(security): validate image files with magic bytes instead of trusting MIME type
Some checks failed
CI / Lint, Type Check, Format & Unit Tests (pull_request) Failing after 1m33s
CI / E2E Tests (pull_request) Successful in 2m41s
4a22853fc1
Replace the empty-MIME-type bypass with magic byte validation. The previous
approach allowed files with empty type or application/octet-stream through
validation unconditionally, which could let attackers upload non-image files
(e.g. SVG with embedded JS, HTML) that bypass compression and get stored.

Now validateImageFile:
1. Rejects files with known-bad MIME types (fast path)
2. Always reads and validates file header magic bytes against known signatures
3. Supports JPEG, PNG, WebP, and HEIC/HEIF detection from binary headers
4. iOS empty-type files are accepted only if magic bytes confirm real image

Update tests to use proper magic byte headers in mock files.

Co-authored-by: Shelley <shelley@exe.dev>
Co-authored-by: Shelley <shelley@exe.dev>
docs(storage): document two-level validation rationale and attack vector
All checks were successful
CI / Lint, Type Check, Format & Unit Tests (pull_request) Successful in 2m0s
CI / E2E Tests (pull_request) Successful in 2m54s
dfa423ebdb
Co-authored-by: Shelley <shelley@exe.dev>
feat(logging): improve server-side logging for item and image API routes
All checks were successful
CI / Lint, Type Check, Format & Unit Tests (pull_request) Successful in 1m59s
CI / E2E Tests (pull_request) Successful in 2m35s
da3b8a7f46
- Add console.info to ESLint allowed methods
- Use console.info for normal flow (request received, auth, success)
- Use console.warn for suspicious inputs (invalid paths, empty MIME types)
- Keep console.error for actual errors
- PATCH /api/items/[id]: log updateData fields, image processing details
  (current count, requested IDs, deleted paths, reorder count)
- POST /api/items: log request body, auth, and created item ID
- POST /api/items/images: convert info-level logs from warn to info
- storage.ts: log successful validation with detected type
- ItemForm.tsx: convert image selection log from warn to info

Co-authored-by: Shelley <shelley@exe.dev>
fix(logging): add logging for MIME type rejection in validateImageFile
All checks were successful
CI / Lint, Type Check, Format & Unit Tests (pull_request) Successful in 2m1s
CI / E2E Tests (pull_request) Successful in 2m37s
6d38e2453f
Co-authored-by: Shelley <shelley@exe.dev>
addison deleted branch fix/ios-image-upload 2026-02-18 06:35:48 -05: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
kwila/market!80
No description provided.