stroke-deepisles-demo / docs /bugs /004-staticfiles-cors-middleware-not-applied.md
VibecoderMcSwaggins's picture
docs(bugs): add BUG-004 - StaticFiles CORS middleware not applied
1977496

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

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.

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

"Each sub-app owns its routers/middleware/lifecycle"

From FastAPI/Starlette Discussion #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:

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:

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:

# 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

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?