| # 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 | |
| 1. Frontend loads successfully at `https://vibecodermcswaggins-stroke-viewer-frontend.hf.space` | |
| 2. Backend health check responds: `{"status":"healthy"}` | |
| 3. Case dropdown populates (GET /api/cases works) | |
| 4. Segmentation job runs successfully (39.5s, returns metrics) | |
| 5. Results panel shows: Case, Dice Score (0.000), Volume (0.00 mL), Time (39.5s) | |
| 6. **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. | |
| ```python | |
| # 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 | |
| 1. Backend logs show all requests succeed with 200 OK | |
| 2. Browser DevTools Network tab would show CORS preflight failure for `/files/*` requests | |
| 3. NiiVue's `loadVolumes()` throws "Failed to fetch" (generic browser CORS error) | |
| ### Starlette Documentation Reference | |
| From [Starlette Middleware Docs](https://www.starlette.io/middleware/): | |
| > "Each sub-app owns its routers/middleware/lifecycle" | |
| From [FastAPI/Starlette Discussion #7319](https://github.com/fastapi/fastapi/discussions/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: | |
| ```python | |
| 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: | |
| ```python | |
| 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: | |
| ```dockerfile | |
| # 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 CORS | |
| - `Dockerfile` - Should set `BACKEND_PUBLIC_URL` explicitly | |
| ## Verification Steps | |
| After fix: | |
| 1. Deploy updated backend to HF Spaces | |
| 2. Clear browser cache | |
| 3. Open frontend, select case, run segmentation | |
| 4. Check browser DevTools β Network tab: | |
| - `/files/*` requests should show `access-control-allow-origin` header | |
| 5. NiiVue should load and display DWI + prediction overlay | |
| ## References | |
| - [FastAPI/Starlette CORS Discussion #7319](https://github.com/fastapi/fastapi/discussions/7319) | |
| - [Starlette Middleware Stack](https://www.starlette.io/middleware/) | |
| - [FastAPI CORS Tutorial](https://fastapi.tiangolo.com/tutorial/cors/) | |
| - [CORSMiddleware not working with mounted apps](https://github.com/fastapi/fastapi/issues/1663) | |
| ## Senior Review Questions | |
| 1. **Solution preference**: Route-based (A) vs ASGI wrapper (B)? | |
| 2. **BACKEND_PUBLIC_URL**: Set in Dockerfile or HF Space settings? | |
| 3. **Testing**: Add integration test for static file CORS headers? | |
| 4. **Monitoring**: How to detect this regression in future? | |