VibecoderMcSwaggins commited on
Commit
1977496
Β·
1 Parent(s): 07db7cc

docs(bugs): add BUG-004 - StaticFiles CORS middleware not applied

Browse files

Root 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
+ ![Screenshot showing the error](../assets/niivue-failed-to-fetch-screenshot.png)
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
- None currently.
 
 
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 | PASS | `r"https://.*stroke-viewer-frontend.*\.hf\.space"` |
 
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. Gateway Timeouts (SOLVED)
 
 
 
 
 
 
 
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
- β”œβ”€β”€ CORS headers on static file response
110
- └── Binary NIfTI files download
 
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