File size: 6,611 Bytes
1977496 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# 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?
|