Bug 004: CORS/CORP Middleware Not Applied to Mounted StaticFiles
Status: OPEN Date Found: 2025-12-12 Severity: CRITICAL (blocks NiiVue from loading NIfTI files) Requires: Senior Review
Symptoms
- Frontend loads successfully at
https://vibecodermcswaggins-stroke-viewer-frontend.hf.space - Backend health check responds:
{"status":"healthy"} - Case dropdown populates (GET /api/cases works)
- Segmentation job runs successfully (39.5s, returns metrics)
- Results panel shows: Case, Dice Score (0.000), Volume (0.00 mL), Time (39.5s)
- NiiVue viewer shows: "Failed to load volume: Failed to fetch"
Root Cause Analysis
The Core Issue: Starlette Middleware Architecture
When FastAPI/Starlette mounts a sub-application (like StaticFiles), the parent app's middleware does NOT propagate to the mounted app.
# main.py - Current implementation
app = FastAPI(...)
# These middlewares only apply to routes on `app` itself
app.add_middleware(CORPMiddleware) # For SharedArrayBuffer
app.add_middleware(CORSMiddleware, allow_origins=CORS_ORIGINS, ...)
# This creates a SEPARATE sub-application with its OWN middleware stack (empty!)
app.mount("/files", StaticFiles(directory=str(RESULTS_DIR)), name="files")
Result:
- API routes (
/api/*) → Get CORS headers ✓ - Static files (
/files/*) → NO CORS headers ✗ (blocked by browser)
Evidence
- Backend logs show all requests succeed with 200 OK
- Browser DevTools Network tab would show CORS preflight failure for
/files/*requests - NiiVue's
loadVolumes()throws "Failed to fetch" (generic browser CORS error)
Starlette Documentation Reference
From Starlette Middleware Docs:
"Each sub-app owns its routers/middleware/lifecycle"
From FastAPI/Starlette Discussion #7319:
"When using
app.mount(), middleware on the parent app may not apply to mounted sub-apps"
Impact
- NiiVue cannot load NIfTI files - Core functionality completely broken
- SharedArrayBuffer may not work - CORP header also missing from static files
- All production users affected - Cross-origin fetch blocked
Proposed Solutions
Solution A: Custom Route Instead of StaticFiles (Recommended)
Replace StaticFiles mount with explicit route handlers that go through the main app's middleware:
from fastapi.responses import FileResponse
@router.get("/files/{job_id}/{case_id}/{filename}")
async def get_result_file(job_id: str, case_id: str, filename: str):
"""Serve NIfTI result files through main app (gets CORS headers)."""
file_path = RESULTS_DIR / job_id / case_id / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(
file_path,
media_type="application/gzip", # .nii.gz
filename=filename,
)
Pros: Simple, uses existing middleware Cons: Less efficient than StaticFiles for large files (no sendfile)
Solution B: ASGI Middleware Wrapper for StaticFiles
Wrap StaticFiles in a custom ASGI app that adds CORS headers:
class CORSStaticFiles:
"""StaticFiles wrapper that adds CORS headers."""
def __init__(self, directory: str, origins: list[str]):
self.static = StaticFiles(directory=directory)
self.origins = origins
async def __call__(self, scope, receive, send):
if scope["type"] == "http":
# Add CORS headers to response
async def send_with_cors(message):
if message["type"] == "http.response.start":
headers = dict(message.get("headers", []))
origin = dict(scope.get("headers", [])).get(b"origin", b"").decode()
if origin in self.origins:
headers[b"access-control-allow-origin"] = origin.encode()
headers[b"cross-origin-resource-policy"] = b"cross-origin"
message["headers"] = list(headers.items())
await send(message)
return await self.static(scope, receive, send_with_cors)
return await self.static(scope, receive, send)
# Usage
app.mount("/files", CORSStaticFiles(str(RESULTS_DIR), CORS_ORIGINS))
Pros: Preserves StaticFiles efficiency Cons: More complex, custom ASGI code
Solution C: Nginx/Caddy Reverse Proxy
Add CORS headers at the reverse proxy level (HF Spaces would need configuration access):
Pros: Most efficient, proper separation of concerns Cons: HF Spaces may not support custom proxy config
Additional Issues Discovered
Issue 1: BACKEND_PUBLIC_URL Not Set
The Dockerfile doesn't set BACKEND_PUBLIC_URL, relying on --proxy-headers and request.base_url. This is fragile:
# Current (fragile)
CMD ["uvicorn", "...:app", "--proxy-headers"]
# Should add (robust)
ENV BACKEND_PUBLIC_URL=https://vibecodermcswaggins-stroke-deepisles-demo.hf.space
Issue 2: chmod "Operation not permitted" Warnings
chmod: changing permissions of '/app/weights/SEALS/nnUNet_trained_models/...': Operation not permitted
Status: Harmless - DeepISLES tries to chmod model weights but fails. The container can still READ the files, which is all that's needed. These are benign warnings, not errors.
Files Affected
src/stroke_deepisles_demo/api/main.py- Needs fix for static file CORSDockerfile- Should setBACKEND_PUBLIC_URLexplicitly
Verification Steps
After fix:
- Deploy updated backend to HF Spaces
- Clear browser cache
- Open frontend, select case, run segmentation
- Check browser DevTools → Network tab:
/files/*requests should showaccess-control-allow-originheader
- NiiVue should load and display DWI + prediction overlay
References
- FastAPI/Starlette CORS Discussion #7319
- Starlette Middleware Stack
- FastAPI CORS Tutorial
- CORSMiddleware not working with mounted apps
Senior Review Questions
- Solution preference: Route-based (A) vs ASGI wrapper (B)?
- BACKEND_PUBLIC_URL: Set in Dockerfile or HF Space settings?
- Testing: Add integration test for static file CORS headers?
- Monitoring: How to detect this regression in future?
