fix: ensure correct Content-Type on photo static files #240

Merged
forgejo_admin merged 1 commit from 210-fix-corb-warnings into main 2026-03-29 04:33:25 +00:00
Contributor

Summary

Replace the bare StaticFiles mount with a custom ImageStaticFiles subclass that uses a hardcoded extension-to-MIME mapping for image files, bypassing Python's mimetypes.guess_type which can return incorrect types on minimal container images. This prevents browsers from triggering CORB (Cross-Origin Read Blocking) warnings when loading player photos cross-origin.

Changes

  • src/basketball_api/static.py (new) -- ImageStaticFiles subclass of Starlette's StaticFiles that overrides file_response to set explicit Content-Type headers for .jpg, .jpeg, .png, and .webp files using a hardcoded IMAGE_MIME_TYPES map. Non-image extensions fall through to default behavior.
  • src/basketball_api/main.py -- Swap StaticFiles import for ImageStaticFiles; update the /uploads/photos mount to use the new class.
  • tests/test_photo_content_type.py (new) -- 6 tests verifying correct Content-Type headers for each image extension, confirming no application/octet-stream leaks, and 404 for missing files.

Test Plan

  • pytest tests/test_photo_content_type.py -v -- all 6 new tests pass
  • Full suite (pytest tests/ -v) -- 683 passed, pre-existing failures unchanged (groupme_sdk missing, DB enum collision)
  • ruff format + ruff check clean
  • Existing upload tests unaffected (no changes to upload routes or directory structure)

Review Checklist

  • ruff format and ruff check pass
  • New tests cover all accepted image extensions (.jpg, .jpeg, .png, .webp)
  • No regression in existing test suite
  • MIME map stays in sync with ALLOWED_EXTENSIONS in routes/upload.py
  • No changes to upload directory structure or URL scheme
  • Companion PR needed in westside-app for crossorigin="anonymous" on <img> tags (separate repo/agent)
  • Predecessor: #207 / PR #208 (CORS fix -- moved StaticFiles mount to module level)

Closes #210

## Summary Replace the bare `StaticFiles` mount with a custom `ImageStaticFiles` subclass that uses a hardcoded extension-to-MIME mapping for image files, bypassing Python's `mimetypes.guess_type` which can return incorrect types on minimal container images. This prevents browsers from triggering CORB (Cross-Origin Read Blocking) warnings when loading player photos cross-origin. ## Changes - **`src/basketball_api/static.py`** (new) -- `ImageStaticFiles` subclass of Starlette's `StaticFiles` that overrides `file_response` to set explicit `Content-Type` headers for `.jpg`, `.jpeg`, `.png`, and `.webp` files using a hardcoded `IMAGE_MIME_TYPES` map. Non-image extensions fall through to default behavior. - **`src/basketball_api/main.py`** -- Swap `StaticFiles` import for `ImageStaticFiles`; update the `/uploads/photos` mount to use the new class. - **`tests/test_photo_content_type.py`** (new) -- 6 tests verifying correct `Content-Type` headers for each image extension, confirming no `application/octet-stream` leaks, and 404 for missing files. ## Test Plan - `pytest tests/test_photo_content_type.py -v` -- all 6 new tests pass - Full suite (`pytest tests/ -v`) -- 683 passed, pre-existing failures unchanged (groupme_sdk missing, DB enum collision) - `ruff format` + `ruff check` clean - Existing upload tests unaffected (no changes to upload routes or directory structure) ## Review Checklist - [x] `ruff format` and `ruff check` pass - [x] New tests cover all accepted image extensions (.jpg, .jpeg, .png, .webp) - [x] No regression in existing test suite - [x] MIME map stays in sync with `ALLOWED_EXTENSIONS` in `routes/upload.py` - [x] No changes to upload directory structure or URL scheme ## Related Notes - Companion PR needed in westside-app for `crossorigin="anonymous"` on `<img>` tags (separate repo/agent) - Predecessor: #207 / PR #208 (CORS fix -- moved StaticFiles mount to module level) ## Related Closes #210
fix: ensure correct Content-Type on served photo files to prevent CORB warnings
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
f3072888ee
Replace bare StaticFiles mount with ImageStaticFiles subclass that uses a
hardcoded extension-to-MIME mapping for image files (.jpg, .jpeg, .png, .webp),
bypassing Python's mimetypes.guess_type which may return wrong types on minimal
container images.  This prevents browsers from triggering CORB (Cross-Origin
Read Blocking) warnings when loading player photos cross-origin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Contributor

QA Review -- PR #240

Scope Check

  • PR addresses backend half of #210 (CORB warnings on photo static files)
  • Frontend crossorigin attribute changes correctly deferred to separate westside-app PR
  • No out-of-scope changes

Code Quality

src/basketball_api/static.py (new)

  • Clean subclass of StaticFiles with single-responsibility override
  • IMAGE_MIME_TYPES map covers all 4 accepted extensions (.jpg, .jpeg, .png, .webp) -- matches ALLOWED_EXTENSIONS in routes/upload.py
  • Comment documents the sync requirement between the two constants
  • file_response signature matches parent class (PathLike type alias vs str | os.PathLike[str] are equivalent at runtime)
  • Correctly delegates to super().file_response() for non-image extensions
  • is_not_modified / NotModifiedResponse handling preserved for cache behavior -- no regression on conditional requests
  • Lazy imports inside the if branch are fine for a rarely-changing code path

src/basketball_api/main.py

  • Clean swap: StaticFiles -> ImageStaticFiles, no other changes
  • Import sorted correctly by ruff

tests/test_photo_content_type.py (new)

  • 6 tests covering all 4 extensions + negative cases (no octet-stream, 404 for missing)
  • Uses the shared db fixture and settings.upload_dir temp directory from conftest -- no mount hacking needed
  • Proper cleanup of seeded files in fixture teardown
  • pytest import of unused pytest removed (ruff would catch this) -- confirmed ruff passes

SOP Compliance

  • ruff format + ruff check clean
  • Tests pass (6/6 new, 683 existing unchanged)
  • Branch naming follows convention: 210-fix-corb-warnings
  • PR body has Closes #210
  • No secrets or env files committed

Nits

None.

VERDICT: APPROVED

## QA Review -- PR #240 ### Scope Check - PR addresses backend half of #210 (CORB warnings on photo static files) - Frontend `crossorigin` attribute changes correctly deferred to separate westside-app PR - No out-of-scope changes ### Code Quality **`src/basketball_api/static.py`** (new) - Clean subclass of `StaticFiles` with single-responsibility override - `IMAGE_MIME_TYPES` map covers all 4 accepted extensions (`.jpg`, `.jpeg`, `.png`, `.webp`) -- matches `ALLOWED_EXTENSIONS` in `routes/upload.py` - Comment documents the sync requirement between the two constants - `file_response` signature matches parent class (`PathLike` type alias vs `str | os.PathLike[str]` are equivalent at runtime) - Correctly delegates to `super().file_response()` for non-image extensions - `is_not_modified` / `NotModifiedResponse` handling preserved for cache behavior -- no regression on conditional requests - Lazy imports inside the `if` branch are fine for a rarely-changing code path **`src/basketball_api/main.py`** - Clean swap: `StaticFiles` -> `ImageStaticFiles`, no other changes - Import sorted correctly by ruff **`tests/test_photo_content_type.py`** (new) - 6 tests covering all 4 extensions + negative cases (no octet-stream, 404 for missing) - Uses the shared `db` fixture and `settings.upload_dir` temp directory from conftest -- no mount hacking needed - Proper cleanup of seeded files in fixture teardown - `pytest` import of unused `pytest` removed (ruff would catch this) -- confirmed ruff passes ### SOP Compliance - [x] `ruff format` + `ruff check` clean - [x] Tests pass (6/6 new, 683 existing unchanged) - [x] Branch naming follows convention: `210-fix-corb-warnings` - [x] PR body has `Closes #210` - [x] No secrets or env files committed ### Nits None. **VERDICT: APPROVED**
forgejo_admin deleted branch 210-fix-corb-warnings 2026-03-29 04:33:25 +00:00
Sign in to join this conversation.
No description provided.