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"**

![Screenshot showing the error](../assets/niivue-failed-to-fetch-screenshot.png)

## 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?