Skip to content

fix: add RootPathMiddleware for proper static file routing with root_…#996

Open
JartanFTW wants to merge 12 commits intosmithyhq:mainfrom
JartanFTW:fix/root-path-static-files
Open

fix: add RootPathMiddleware for proper static file routing with root_…#996
JartanFTW wants to merge 12 commits intosmithyhq:mainfrom
JartanFTW:fix/root-path-static-files

Conversation

@JartanFTW
Copy link
Copy Markdown

@JartanFTW JartanFTW commented Jan 16, 2026

…path

When FastAPI/Starlette is deployed with root_path configured (e.g., behind a reverse proxy), static file requests could fail with 404 because Starlette's internal routing couldn't properly resolve paths for nested mounts.

This middleware normalizes request paths by prepending root_path when it's set but not included in the request path, ensuring StaticFiles and other nested mounts work correctly.

Fixes #538

All pytest cases pass. I also tested that this fix works via a docker-compose deployment on JartanFTW/sqladmin/tree/test/root-path-deployment

@JartanFTW
Copy link
Copy Markdown
Author

For whatever reason this issue has disappeared even without the fix 😂 Not sure what's going on, or if this PR is necessary.

Copy link
Copy Markdown
Collaborator

@aminalaee aminalaee left a comment

Choose a reason for hiding this comment

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

Can you please add a test to verify it's working as expected?

@JartanFTW
Copy link
Copy Markdown
Author

JartanFTW commented Feb 25, 2026

Can you please add a test to verify it's working as expected?

From my original post:

All pytest cases pass. I also tested that this fix works via a docker-compose deployment on JartanFTW/sqladmin/tree/test/root-path-deployment

Can you please clarify what specific testing you want done?

@mmzeynalli
Copy link
Copy Markdown
Member

@JartanFTW add a test which after redirecting to some page, check page url, and make sure that, root_path is in the URL

@aminalaee aminalaee added the waiting-for-feedback Waiting feedback/answer/updates from contributor label Mar 30, 2026
@mmzeynalli
Copy link
Copy Markdown
Member

@JartanFTW any updates? Could you also add support for starlette 1.0 which was recently released? You just need to change pyproject.toml:

dependencies = [
  "starlette==0.49.3,<0.50.0; python_version == '3.9'",
  "starlette>=0.50; python_version >= '3.10'",
  "wtforms >=3.1, <3.2",
  "jinja2",
  "python-multipart",
  "sqlalchemy >= 2.0",
]

@JartanFTW
Copy link
Copy Markdown
Author

JartanFTW commented Apr 5, 2026

It's on my todo list, just very busy at the moment and this is low priority for me. Apologies!
I'll try to get to it later today.

JartanFTW and others added 4 commits April 5, 2026 13:48
…path

When FastAPI/Starlette is deployed with root_path configured (e.g., behind a
reverse proxy), static file requests could fail with 404 because Starlette's
internal routing couldn't properly resolve paths for nested mounts.

This middleware normalizes request paths by prepending root_path when it's
set but not included in the request path, ensuring StaticFiles and other
nested mounts work correctly.

Fixes smithyhq#538
The middleware was on the inner admin Starlette app, where Mount had
already appended base_url to root_path, causing double-prefix rewrites.
Moving it to the outer app and scoping it to admin paths fixes routing
for static files and all admin routes behind a reverse proxy.
@JartanFTW JartanFTW force-pushed the fix/root-path-static-files branch from d3f1378 to caad855 Compare April 5, 2026 19:24
@JartanFTW
Copy link
Copy Markdown
Author

JartanFTW commented Apr 5, 2026

Design decisions

  1. Middleware moved from inner to outer app — The inner admin Starlette app receives root_path already appended with base_url by Starlette's Mount. This caused the middleware to prepend /api/v1/admin + /admin/... = double prefix. Moving it to the outer app means it sees the original root_path before any Mount processing.
  2. Scoped to path_prefix — Since the middleware is on the user's app, we only rewrite paths starting with base_url to avoid touching their routes. The check uses the combined root_path + base_url prefix to determine whether rewriting is needed, which also correctly handles the edge case where root_path equals base_url. User routes already work fine with root_path because they don't go through nested Mount routing.
  3. Starlette dependency pinned by Python version — starlette==0.49.3 for Python 3.9 (last supported version), starlette>=0.50 for 3.10+.

What the middleware does not cover

  • WebSocket scope — Consistent with the rest of the library which is HTTP-only.
  • Non-admin mounts — By design. Only admin routes need the fix because they use nested Starlette Mount routing. Top-levelroutes handle root_path natively.

Open question: Starlette pin for Python 3.9
The current pin is starlette==0.49.3 for Python 3.9, as requested by @mmzeynalli. Should this be ~=0.49.3 instead? That would allow patch releases (e.g., a hypothetical 0.49.4 security fix) while still staying within the 0.49.x line that supports Python 3.9. ==0.49.3 locks users out of any future patch.

@JartanFTW JartanFTW requested a review from aminalaee April 5, 2026 19:48
Prevents the middleware from matching routes like /admin-panel when
path_prefix is /admin by requiring an exact match or a / boundary.
@JartanFTW
Copy link
Copy Markdown
Author

Ok now I'm actually done. Let me know how it looks


session_maker = sessionmaker(bind=engine)

Base = declarative_base() # type: ignore
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I know this is copied, but no need for type: ignore. I will remove others as well.

@mmzeynalli mmzeynalli added ready-to-merge Approved and ready to merge. Will be published with the next release. and removed waiting-for-feedback Waiting feedback/answer/updates from contributor labels Apr 5, 2026
@mmzeynalli
Copy link
Copy Markdown
Member

Open question: Starlette pin for Python 3.9 The current pin is starlette==0.49.3 for Python 3.9, as requested by @mmzeynalli. Should this be ~=0.49.3 instead? That would allow patch releases (e.g., a hypothetical 0.49.4 security fix) while still staying within the 0.49.x line that supports Python 3.9. ==0.49.3 locks users out of any future patch.

yes, totally agree. ~ us bettter than ==.

Allow compatible patch releases instead of exact pin, per review feedback.
Per review feedback from mmzeynalli.
@JartanFTW JartanFTW requested a review from mmzeynalli April 6, 2026 19:49
…upport

The ajax_lookup_url for Select2 relationship dropdowns was computed once
at startup via urljoin, which never included root_path. Behind a reverse
proxy, the AJAX data-url in HTML was wrong, causing 404s.

Move URL generation to the Jinja2 template layer using url_for(), matching
how every other URL in sqladmin is generated. The pre-computed URL on
ModelView is kept as a fallback for backwards compatibility with custom
templates.
@JartanFTW JartanFTW requested a review from mmzeynalli April 7, 2026 21:39
@mmzeynalli
Copy link
Copy Markdown
Member

mmzeynalli commented Apr 7, 2026

@JartanFTW would @MaximDementyev's solution work, instead of this much change?

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

Labels

ready-to-merge Approved and ready to merge. Will be published with the next release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Adding root_path breaks admin page

3 participants