Skip to content

fix: reflect request origin in CORS responses instead of wildcard#375

Open
mkitti wants to merge 1 commit into
JaneliaSciComp:mainfrom
mkitti:mkitti-fix-cors-headers
Open

fix: reflect request origin in CORS responses instead of wildcard#375
mkitti wants to merge 1 commit into
JaneliaSciComp:mainfrom
mkitti:mkitti-fix-cors-headers

Conversation

@mkitti
Copy link
Copy Markdown
Contributor

@mkitti mkitti commented May 30, 2026

Summary

  • Replace allow_origins=["*"] with allow_origin_regex=r".*" in the Starlette CORSMiddleware so the server reflects the specific request origin instead of returning *
  • Add Accept-Ranges and Content-Length to expose_headers

Problem

Starlette 0.48 sends Access-Control-Allow-Origin: * combined with Access-Control-Allow-Credentials: true when allow_origins=["*"] is configured. Browsers reject this combination even for non-credentialed requests (e.g. HTTP Range reads from a cross-origin viewer).

This means any client served from a different origin — such as zipglancer, a companion viewer for ZIP/OME-Zarr archives — cannot make range requests to fileglancer's /files/ shared path endpoint.

Fix

allow_origin_regex=r".*" causes Starlette to reflect the actual request origin in Access-Control-Allow-Origin, which is valid alongside Allow-Credentials: true and accepted by all browsers.

The additional exposed headers (Accept-Ranges, Content-Length) allow cross-origin clients to read range-request metadata directly without workarounds.

Test plan

  • Run pixi run node-install && pixi run node-eslint-check — passes (0 errors)
  • Open fileglancer and create a data link to a .zip or .ozx file
  • Open the data link URL from a cross-origin client (e.g. zipglancer at https://janeliascicomp.github.io/zipglancer/) and confirm the archive loads without CORS errors

🤖 Generated with Claude Code

Starlette 0.48 sends Access-Control-Allow-Origin: * combined with
Access-Control-Allow-Credentials: true when allow_origins=['*'] is
used. Browsers reject this combination even for non-credentialed
requests (e.g. HTTP Range reads from a cross-origin viewer like
zipglancer).

Switching to allow_origin_regex=r'.*' causes Starlette to reflect the
specific request origin instead of *, which is valid alongside
Allow-Credentials: true.

Also exposes Accept-Ranges and Content-Length in CORS responses so
cross-origin clients can read range-request metadata directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mkitti
Copy link
Copy Markdown
Contributor Author

mkitti commented May 30, 2026

I needed this when running fileglancer locally, but I am not sure if this change is needed with fileglancer deployed:
https://janeliascicomp.github.io/zipglancer/?url=https%3A%2F%2Ffileglancer.int.janelia.org%2Ffiles%2FkhVBD0vfObEZppzM%2Fozx_demo%2Fbonsai.ozx

@mkitti
Copy link
Copy Markdown
Contributor Author

mkitti commented May 30, 2026

@krokicki krokicki requested a review from neomorphic May 31, 2026 12:25
Copy link
Copy Markdown
Member

@neomorphic neomorphic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will work, but it is using an anti-pattern as described here. Since this is an internal application, that is probably fine.

@mkitti
Copy link
Copy Markdown
Contributor Author

mkitti commented Jun 1, 2026

Why does it work with the deployed Fileglancer as in https://janeliascicomp.github.io/zipglancer/?url=https%3A%2F%2Ffileglancer.int.janelia.org%2Ffiles%2FkhVBD0vfObEZppzM%2Fozx_demo%2Fbonsai.ozx ?

Perhaps we only need this setting for development use (pixi run dev-launch, or fileglancer start).

@neomorphic
Copy link
Copy Markdown
Member

Why does it work with the deployed Fileglancer as in https://janeliascicomp.github.io/zipglancer/?url=https%3A%2F%2Ffileglancer.int.janelia.org%2Ffiles%2FkhVBD0vfObEZppzM%2Fozx_demo%2Fbonsai.ozx ?

It works with the deployed version on fileglancer.int.janelia.org, because that installation is already returning mirrored domains and the Allow-Credentials: true header, served from Starlette 1.0.0. I think if the dev server was to run Starlette 1.0.0 you wouldn't need this patch

@neomorphic
Copy link
Copy Markdown
Member

@allison-truhlar There appears to be an issue with the dependency chain. Paraphrased from Claude:

Local dev env (conda-forge):

  • Your conda pin is fastapi = ">=0.119.1,<0.120" (pyproject.toml:176) → resolves fastapi 0.119.1.
  • fastapi 0.119.1's own metadata: Requires-Dist: starlette<0.49.0,>=0.40.0 → so starlette can't go past 0.48.0.

Production (PyPI, via the hub installing fileglancer==2.8.1):

  • fileglancer's PyPI dependency is "fastapi >=0.119.1" (pyproject.toml:30) — no upper bound → PyPI floats up to fastapi 0.135.3.
  • fastapi 0.135.3's metadata: Requires-Dist: starlette>=0.46.0 — no upper cap → resolves starlette 1.0.0.

So starlette is "pinned" to 0.48 in dev purely because the <0.120 cap on fastapi forces fastapi 0.119.1, whose metadata forbids starlette ≥ 0.49. conda-forge isn't the limiter — it carries newer fastapi/starlette; your own cap blocks the dev env from
reaching them.

The real problem: two inconsistent declarations

You have the same dependencies declared twice, with different bounds, and dev vs prod use different ones:

┌─────────┬─────────────────────────────────────────┬───────────────────────────────────────────┐
│ Package │ PyPI [project.dependencies] (prod path) │ conda [tool.pixi.dependencies] (dev path) │
├─────────┼─────────────────────────────────────────┼───────────────────────────────────────────┤
│ fastapi │ >=0.119.1 (open) → 0.135.3              │ >=0.119.1,<0.120 → 0.119.1                │
├─────────┼─────────────────────────────────────────┼───────────────────────────────────────────┤
│ uvicorn │ >=0.38.0 (open)                         │ >=0.38.0,<0.39                            │
└─────────┴─────────────────────────────────────────┴───────────────────────────────────────────┘

Prod installs fileglancer from PyPI (the hub's pypi-dependencies), so it follows the open-ended column and floats to fastapi 0.135 / starlette 1.0.0. Dev installs from conda-forge with the capped column and stays at fastapi 0.119 / starlette 0.48. That
divergence is the entire "works in prod, breaks in dev" CORS story — and it's not just starlette; your dev env is a full 16 minor versions behind prod on FastAPI itself.

The fix

Relax (or remove) the conda upper cap so the dev env can resolve the same FastAPI/Starlette as prod, then re-lock:

  # [tool.pixi.dependencies]
  fastapi = ">=0.135.0,<0.136"   # or simply ">=0.119.1" to track prod's open bound
  uvicorn = ">=0.38.0"

then pixi update fastapi starlette uvicorn (or pixi update) to refresh pixi.lock. After that, dev-launch would run Starlette 1.0.0 and reproduce production's origin-reflection behavior.

Two caveats worth checking before you bump:

  • FastAPI 0.119 → 0.135 and Starlette 0.48 → 1.0.0 is a major Starlette release; there may be behavior changes beyond CORS. Worth running the test suite after.
  • Ideally, make the two declarations agree (or have the conda one mirror the PyPI bound) so this drift doesn't silently reappear.

@allison-truhlar
Copy link
Copy Markdown
Collaborator

Okay - it looks like a straightforward fix - just make the dev env requirements match the PyPi requirements. I can try this out in a branch and then Mark can try it out with Zipglancer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants