Commit
Β·
1977496
1
Parent(s):
07db7cc
docs(bugs): add BUG-004 - StaticFiles CORS middleware not applied
Browse filesRoot cause analysis for "Failed to load volume: Failed to fetch" error.
Discovery: FastAPI/Starlette middleware doesn't propagate to mounted
sub-applications. The CORSMiddleware and CORPMiddleware only apply to
API routes, not the StaticFiles mount at /files/*.
Impact: NiiVue cannot fetch NIfTI files cross-origin (CRITICAL)
Proposed solutions:
A. Replace StaticFiles with explicit FileResponse routes (recommended)
B. ASGI middleware wrapper for StaticFiles
C. Reverse proxy CORS headers
Status: OPEN - Awaiting senior review
docs/bugs/004-staticfiles-cors-middleware-not-applied.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Bug 004: CORS/CORP Middleware Not Applied to Mounted StaticFiles
|
| 2 |
+
|
| 3 |
+
**Status**: OPEN
|
| 4 |
+
**Date Found**: 2025-12-12
|
| 5 |
+
**Severity**: CRITICAL (blocks NiiVue from loading NIfTI files)
|
| 6 |
+
**Requires**: Senior Review
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## Symptoms
|
| 11 |
+
|
| 12 |
+
1. Frontend loads successfully at `https://vibecodermcswaggins-stroke-viewer-frontend.hf.space`
|
| 13 |
+
2. Backend health check responds: `{"status":"healthy"}`
|
| 14 |
+
3. Case dropdown populates (GET /api/cases works)
|
| 15 |
+
4. Segmentation job runs successfully (39.5s, returns metrics)
|
| 16 |
+
5. Results panel shows: Case, Dice Score (0.000), Volume (0.00 mL), Time (39.5s)
|
| 17 |
+
6. **NiiVue viewer shows: "Failed to load volume: Failed to fetch"**
|
| 18 |
+
|
| 19 |
+

|
| 20 |
+
|
| 21 |
+
## Root Cause Analysis
|
| 22 |
+
|
| 23 |
+
### The Core Issue: Starlette Middleware Architecture
|
| 24 |
+
|
| 25 |
+
When FastAPI/Starlette mounts a sub-application (like `StaticFiles`), the parent app's middleware **does NOT propagate** to the mounted app.
|
| 26 |
+
|
| 27 |
+
```python
|
| 28 |
+
# main.py - Current implementation
|
| 29 |
+
app = FastAPI(...)
|
| 30 |
+
|
| 31 |
+
# These middlewares only apply to routes on `app` itself
|
| 32 |
+
app.add_middleware(CORPMiddleware) # For SharedArrayBuffer
|
| 33 |
+
app.add_middleware(CORSMiddleware, allow_origins=CORS_ORIGINS, ...)
|
| 34 |
+
|
| 35 |
+
# This creates a SEPARATE sub-application with its OWN middleware stack (empty!)
|
| 36 |
+
app.mount("/files", StaticFiles(directory=str(RESULTS_DIR)), name="files")
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
**Result**:
|
| 40 |
+
- API routes (`/api/*`) β Get CORS headers β
|
| 41 |
+
- Static files (`/files/*`) β NO CORS headers β (blocked by browser)
|
| 42 |
+
|
| 43 |
+
### Evidence
|
| 44 |
+
|
| 45 |
+
1. Backend logs show all requests succeed with 200 OK
|
| 46 |
+
2. Browser DevTools Network tab would show CORS preflight failure for `/files/*` requests
|
| 47 |
+
3. NiiVue's `loadVolumes()` throws "Failed to fetch" (generic browser CORS error)
|
| 48 |
+
|
| 49 |
+
### Starlette Documentation Reference
|
| 50 |
+
|
| 51 |
+
From [Starlette Middleware Docs](https://www.starlette.io/middleware/):
|
| 52 |
+
> "Each sub-app owns its routers/middleware/lifecycle"
|
| 53 |
+
|
| 54 |
+
From [FastAPI/Starlette Discussion #7319](https://github.com/fastapi/fastapi/discussions/7319):
|
| 55 |
+
> "When using `app.mount()`, middleware on the parent app may not apply to mounted sub-apps"
|
| 56 |
+
|
| 57 |
+
## Impact
|
| 58 |
+
|
| 59 |
+
- **NiiVue cannot load NIfTI files** - Core functionality completely broken
|
| 60 |
+
- **SharedArrayBuffer may not work** - CORP header also missing from static files
|
| 61 |
+
- **All production users affected** - Cross-origin fetch blocked
|
| 62 |
+
|
| 63 |
+
## Proposed Solutions
|
| 64 |
+
|
| 65 |
+
### Solution A: Custom Route Instead of StaticFiles (Recommended)
|
| 66 |
+
|
| 67 |
+
Replace `StaticFiles` mount with explicit route handlers that go through the main app's middleware:
|
| 68 |
+
|
| 69 |
+
```python
|
| 70 |
+
from fastapi.responses import FileResponse
|
| 71 |
+
|
| 72 |
+
@router.get("/files/{job_id}/{case_id}/{filename}")
|
| 73 |
+
async def get_result_file(job_id: str, case_id: str, filename: str):
|
| 74 |
+
"""Serve NIfTI result files through main app (gets CORS headers)."""
|
| 75 |
+
file_path = RESULTS_DIR / job_id / case_id / filename
|
| 76 |
+
if not file_path.exists():
|
| 77 |
+
raise HTTPException(status_code=404, detail="File not found")
|
| 78 |
+
return FileResponse(
|
| 79 |
+
file_path,
|
| 80 |
+
media_type="application/gzip", # .nii.gz
|
| 81 |
+
filename=filename,
|
| 82 |
+
)
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
**Pros**: Simple, uses existing middleware
|
| 86 |
+
**Cons**: Less efficient than StaticFiles for large files (no sendfile)
|
| 87 |
+
|
| 88 |
+
### Solution B: ASGI Middleware Wrapper for StaticFiles
|
| 89 |
+
|
| 90 |
+
Wrap `StaticFiles` in a custom ASGI app that adds CORS headers:
|
| 91 |
+
|
| 92 |
+
```python
|
| 93 |
+
class CORSStaticFiles:
|
| 94 |
+
"""StaticFiles wrapper that adds CORS headers."""
|
| 95 |
+
|
| 96 |
+
def __init__(self, directory: str, origins: list[str]):
|
| 97 |
+
self.static = StaticFiles(directory=directory)
|
| 98 |
+
self.origins = origins
|
| 99 |
+
|
| 100 |
+
async def __call__(self, scope, receive, send):
|
| 101 |
+
if scope["type"] == "http":
|
| 102 |
+
# Add CORS headers to response
|
| 103 |
+
async def send_with_cors(message):
|
| 104 |
+
if message["type"] == "http.response.start":
|
| 105 |
+
headers = dict(message.get("headers", []))
|
| 106 |
+
origin = dict(scope.get("headers", [])).get(b"origin", b"").decode()
|
| 107 |
+
if origin in self.origins:
|
| 108 |
+
headers[b"access-control-allow-origin"] = origin.encode()
|
| 109 |
+
headers[b"cross-origin-resource-policy"] = b"cross-origin"
|
| 110 |
+
message["headers"] = list(headers.items())
|
| 111 |
+
await send(message)
|
| 112 |
+
return await self.static(scope, receive, send_with_cors)
|
| 113 |
+
return await self.static(scope, receive, send)
|
| 114 |
+
|
| 115 |
+
# Usage
|
| 116 |
+
app.mount("/files", CORSStaticFiles(str(RESULTS_DIR), CORS_ORIGINS))
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
**Pros**: Preserves StaticFiles efficiency
|
| 120 |
+
**Cons**: More complex, custom ASGI code
|
| 121 |
+
|
| 122 |
+
### Solution C: Nginx/Caddy Reverse Proxy
|
| 123 |
+
|
| 124 |
+
Add CORS headers at the reverse proxy level (HF Spaces would need configuration access):
|
| 125 |
+
|
| 126 |
+
**Pros**: Most efficient, proper separation of concerns
|
| 127 |
+
**Cons**: HF Spaces may not support custom proxy config
|
| 128 |
+
|
| 129 |
+
## Additional Issues Discovered
|
| 130 |
+
|
| 131 |
+
### Issue 1: BACKEND_PUBLIC_URL Not Set
|
| 132 |
+
|
| 133 |
+
The Dockerfile doesn't set `BACKEND_PUBLIC_URL`, relying on `--proxy-headers` and `request.base_url`. This is fragile:
|
| 134 |
+
|
| 135 |
+
```dockerfile
|
| 136 |
+
# Current (fragile)
|
| 137 |
+
CMD ["uvicorn", "...:app", "--proxy-headers"]
|
| 138 |
+
|
| 139 |
+
# Should add (robust)
|
| 140 |
+
ENV BACKEND_PUBLIC_URL=https://vibecodermcswaggins-stroke-deepisles-demo.hf.space
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
### Issue 2: chmod "Operation not permitted" Warnings
|
| 144 |
+
|
| 145 |
+
```
|
| 146 |
+
chmod: changing permissions of '/app/weights/SEALS/nnUNet_trained_models/...': Operation not permitted
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
**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.
|
| 150 |
+
|
| 151 |
+
## Files Affected
|
| 152 |
+
|
| 153 |
+
- `src/stroke_deepisles_demo/api/main.py` - Needs fix for static file CORS
|
| 154 |
+
- `Dockerfile` - Should set `BACKEND_PUBLIC_URL` explicitly
|
| 155 |
+
|
| 156 |
+
## Verification Steps
|
| 157 |
+
|
| 158 |
+
After fix:
|
| 159 |
+
1. Deploy updated backend to HF Spaces
|
| 160 |
+
2. Clear browser cache
|
| 161 |
+
3. Open frontend, select case, run segmentation
|
| 162 |
+
4. Check browser DevTools β Network tab:
|
| 163 |
+
- `/files/*` requests should show `access-control-allow-origin` header
|
| 164 |
+
5. NiiVue should load and display DWI + prediction overlay
|
| 165 |
+
|
| 166 |
+
## References
|
| 167 |
+
|
| 168 |
+
- [FastAPI/Starlette CORS Discussion #7319](https://github.com/fastapi/fastapi/discussions/7319)
|
| 169 |
+
- [Starlette Middleware Stack](https://www.starlette.io/middleware/)
|
| 170 |
+
- [FastAPI CORS Tutorial](https://fastapi.tiangolo.com/tutorial/cors/)
|
| 171 |
+
- [CORSMiddleware not working with mounted apps](https://github.com/fastapi/fastapi/issues/1663)
|
| 172 |
+
|
| 173 |
+
## Senior Review Questions
|
| 174 |
+
|
| 175 |
+
1. **Solution preference**: Route-based (A) vs ASGI wrapper (B)?
|
| 176 |
+
2. **BACKEND_PUBLIC_URL**: Set in Dockerfile or HF Space settings?
|
| 177 |
+
3. **Testing**: Add integration test for static file CORS headers?
|
| 178 |
+
4. **Monitoring**: How to detect this regression in future?
|
docs/bugs/README.md
CHANGED
|
@@ -4,7 +4,9 @@ This directory tracks bugs found during deployment to HuggingFace Spaces.
|
|
| 4 |
|
| 5 |
## Active Bugs
|
| 6 |
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
|
| 9 |
## Fixed Bugs
|
| 10 |
|
|
@@ -20,7 +22,8 @@ Last audit: 2025-12-12
|
|
| 20 |
|
| 21 |
| Check | Status | Notes |
|
| 22 |
|-------|--------|-------|
|
| 23 |
-
| CORS regex matches both URL formats |
|
|
|
|
| 24 |
| All URLs use HTTPS | PASS | `--proxy-headers` flag in Dockerfile |
|
| 25 |
| File outputs to /tmp/ | PASS | Uses `/tmp/stroke-results/` |
|
| 26 |
| Static files mounted after dir exists | PASS | `mkdir()` before `app.mount()` in main.py |
|
|
@@ -61,7 +64,14 @@ Based on research and experience, here are common issues to watch for:
|
|
| 61 |
- `SPACE_ID` contains the space identifier
|
| 62 |
- Use these to detect production environment
|
| 63 |
|
| 64 |
-
### 6.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
- HF Spaces proxy has ~60 second timeout
|
| 66 |
- Solution: Async job queue pattern with polling
|
| 67 |
- POST returns immediately with job ID
|
|
@@ -106,8 +116,9 @@ The complete flow from frontend to backend and back:
|
|
| 106 |
|
| 107 |
7. NiiVue fetches static files
|
| 108 |
βββ Cross-origin fetch to backend /files/...
|
| 109 |
-
βββ
|
| 110 |
-
|
|
|
|
| 111 |
|
| 112 |
8. Viewer displays
|
| 113 |
βββ NIfTI volumes rendered in WebGL canvas
|
|
|
|
| 4 |
|
| 5 |
## Active Bugs
|
| 6 |
|
| 7 |
+
| ID | Title | Severity | Status |
|
| 8 |
+
|----|-------|----------|--------|
|
| 9 |
+
| [004](./004-staticfiles-cors-middleware-not-applied.md) | CORS/CORP middleware not applied to mounted StaticFiles | **CRITICAL** | OPEN - Awaiting Senior Review |
|
| 10 |
|
| 11 |
## Fixed Bugs
|
| 12 |
|
|
|
|
| 22 |
|
| 23 |
| Check | Status | Notes |
|
| 24 |
|-------|--------|-------|
|
| 25 |
+
| CORS regex matches both URL formats | N/A | Replaced with exact-match list (PR #38) |
|
| 26 |
+
| **CORS on StaticFiles mount** | **FAIL** | BUG-004: Middleware doesn't apply to mounted apps |
|
| 27 |
| All URLs use HTTPS | PASS | `--proxy-headers` flag in Dockerfile |
|
| 28 |
| File outputs to /tmp/ | PASS | Uses `/tmp/stroke-results/` |
|
| 29 |
| Static files mounted after dir exists | PASS | `mkdir()` before `app.mount()` in main.py |
|
|
|
|
| 64 |
- `SPACE_ID` contains the space identifier
|
| 65 |
- Use these to detect production environment
|
| 66 |
|
| 67 |
+
### 6. chmod "Operation not permitted" Warnings (HARMLESS)
|
| 68 |
+
DeepISLES tries to chmod model weight files but fails due to container permissions:
|
| 69 |
+
```
|
| 70 |
+
chmod: changing permissions of '/app/weights/SEALS/...': Operation not permitted
|
| 71 |
+
```
|
| 72 |
+
These are **benign warnings**, not errors. The container can still READ the files.
|
| 73 |
+
|
| 74 |
+
### 7. Gateway Timeouts (SOLVED)
|
| 75 |
- HF Spaces proxy has ~60 second timeout
|
| 76 |
- Solution: Async job queue pattern with polling
|
| 77 |
- POST returns immediately with job ID
|
|
|
|
| 116 |
|
| 117 |
7. NiiVue fetches static files
|
| 118 |
βββ Cross-origin fetch to backend /files/...
|
| 119 |
+
βββ β οΈ BUG-004: StaticFiles mount doesn't get CORS headers!
|
| 120 |
+
βββ Browser blocks fetch (no Access-Control-Allow-Origin)
|
| 121 |
+
βββ "Failed to load volume: Failed to fetch"
|
| 122 |
|
| 123 |
8. Viewer displays
|
| 124 |
βββ NIfTI volumes rendered in WebGL canvas
|