Skip to content

Fix hash route URL strategy returning 200 instead of 404 for unknown paths - #6954

Merged
FeodorFitsner merged 7 commits into
flet-1.1.0from
fix/hash-routing-404-2796
Oct 11, 2026
Merged

FeodorFitsner merged 7 commits into
flet-1.1.0from
fix/hash-routing-404-2796

Conversation

@ndonkoHenri

@ndonkoHenri ndonkoHenri commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2796

The bug

A Flet web app is a single-page app, so the static file server serves index.html for any unknown route-like path (/products/1) and lets the client route it. In #2796, @FeodorFitsner pointed out that a real 404 is only possible with the hash route URL strategy, where routes live after the # (/#/products/1) and never reach the server.

That never worked. With route_url_strategy="hash", FletStaticFiles.lookup_path() still fell back to index.html for every missing path, because the fallback never checked the strategy. /invalidurl, /products/1, /page.html and /sub-app/nope all returned 200 OK with the app, exactly as in path mode.

The fix

  • Real 404s in hash mode. FletStaticFiles skips the index.html fallback when the strategy is hash, whether it's set by argument or by FLET_WEB_ROUTE_URL_STRATEGY. A path that isn't a real file now gets a real 404, with the assets directory's 404.html as the body if there is one. The mount root (/, /sub-app/) still serves the app.
  • Path mode unchanged. Path mode, the default, is untouched. Its 404s for paths whose last segment contains a dot also keep index.html as the body, so deep links like /users/john.doe still load the app.
  • String values. route_url_strategy and web_renderer given as strings, e.g. route_url_strategy="hash" as in the routing guide, made every request to flet.fastapi.app() and ft.run(..., export_asgi_app=True) fail with a 500 ('str' object has no attribute 'value'). FletStaticFiles now converts them to the enums, which covers both entry points.
  • Login return URL. In hash mode, page.login(redirect_to_page=True) sent the browser back to a path URL (/store), because the Python side didn't know the strategy. That lost the route, and with the 404s above it would land on a 404. flet.fastapi.app() now gives each session the strategy it serves, environment variable included, and login returns to /#/store, or /sub-app/#/store for a sub-app.
  • Login base path. The return URL's base path is now computed like the served <base href>, from proxy_path plus the ASGI root path, instead of from the WebSocket URL. In both modes it now keeps a prefix that a proxy strips (proxy_path), and it ignores the folder of a nested FLET_WEBSOCKET_HANDLER_ENDPOINT such as socket/ws.
  • Docs. The routing guide named the environment variable FLET_ROUTE_URL_STRATEGY; it is FLET_WEB_ROUTE_URL_STRATEGY.
  • page.login() docstring. It only said "Starts OAuth flow." It now explains both ways the method works: the authorization code flow (it returns before sign-in finishes and needs Flet's web server) and restoring a saved token. It also documents every argument and the return value, and adds short comments on the steps in the method.
  • Connection attribute docs. page_base_path, route_url_strategy and local_data_transport now have attribute docstrings instead of comments.

Behaviour changes

All of these apply only to apps using the hash route URL strategy and served by Flet's Python web server: ft.run(..., view=ft.AppView.WEB_BROWSER), flet run --web, ft.run(..., export_asgi_app=True) or flet.fastapi.app(). Path mode, desktop and mobile apps, and flet build web / flet publish sites on a static host are not affected.

  1. A path-style URL of a hash-mode app now returns 404. A typed URL, bookmark or external link such as https://myapp.dev/store used to load the app at route /, silently dropping /store. It now returns 404. Proper hash URLs (https://myapp.dev/#/store) and in-app navigation (page.navigate(), push_route(), back/forward) never ask the server for the route and work as before.
  2. With a root app mounted, a sub-app's URL without the trailing slash returns 404. With app.mount("/sub-app", ...) and app.mount("/", ...), Starlette's Mount("/sub-app") doesn't match the bare /sub-app, so the root app handles it. Before, that served the root app with 200. Now it's a 404. /sub-app/ works as before, and without a root mount Starlette still redirects /sub-app to /sub-app/. A redirect route before the mounts avoids it:
    @app.get("/sub-app", include_in_schema=False)
    async def sub_app_root():
        return RedirectResponse("/sub-app/")

An earlier version of this list had a third item: page.login(redirect_to_page=True) landing on a 404 in hash mode. That is now fixed in this PR (see "Login return URL" above).

Testing

  • TestClient: status codes against a real flet.fastapi app with root and sub-app mounts, both strategies, with and without a 404.html in the assets dir.
  • Real servers: flet run --web servers checked with curl and headless Chrome. In hash mode, /#/store renders route /store and /invalidurl returns 404. On flet-1.1.0, /invalidurl loads the app.
  • Draft tests (not part of this PR): 18 cases covering hash and path mode, 404.html, the env var and the string forms. 9 fail on flet-1.1.0, and all pass with this change.
  • Login, draft tests (not part of this PR): 24 cases. Most run the whole server flow over TestClient: register a client, page.login(), /oauth_callback answering 307 with the flet_oauth_state cookie, reconnect, and the token request with the code. They cover root, sub-app and nested mounts, root_path, proxy_path, a nested WebSocket endpoint, a route with a query, and the environment variable overriding the argument both ways. All pass. On the previous head, every hash-mode case fails, as do path mode with proxy_path and with a nested WebSocket endpoint.
  • Login, real browser: headless Chrome with a fake OAuth provider and the flet-web 1.0.4 client, against root and /sub-app mounts and behind a proxy that strips /prefix (with proxy_path="/prefix"). Both strategies ran with an instant provider redirect, and hash mode at the root and /sub-app also ran with a provider page in between. Every run returns to /#/store, /sub-app/#/store or /prefix/#/store (path mode: /store, /sub-app/store, /prefix/store), resumes the same session, and fires on_login on route /store. On the previous head, hash mode returns to /store, which is a 404, and behind the proxy both modes return to /store, outside the prefix.
  • Suites: the flet-web and flet suites pass.

Summary by Sourcery

Fix hash-mode web routing and OAuth redirects so unknown server paths return 404 and login returns users to the correct application route.

Bug Fixes:

  • Return real 404 responses for unknown paths when Flet web apps use the hash route URL strategy while preserving path-strategy SPA routing behavior.
  • Correct OAuth login return URLs for hash routing, mounted applications, proxy prefixes, and nested deployment paths.
  • Allow string values for web renderer and route URL strategy configuration without causing ASGI requests to fail.

Enhancements:

  • Expose the served route strategy and page base path through the connection so login redirects can follow the deployed app URL.
  • Expand the page.login() documentation to describe OAuth flows, parameters, and return behavior.

Documentation:

  • Correct the documented environment variable name for configuring the web route URL strategy.

With hash routing the client never asks the server for route paths, but
FletStaticFiles still served index.html with 200 OK for any missing
path. Skip the SPA fallback in hash mode, so unknown paths get a real
404 (the assets dir's 404.html if there is one).
FletStaticFiles stored route_url_strategy and web_renderer as given, and
patch_index_html reads their .value, so a string such as "hash" made
every request fail with a 500. flet.fastapi.app() passes the arguments
through unchanged, and ft.run(..., export_asgi_app=True) returns before
it converts strings. Convert them to the enums in FletStaticFiles, which
covers both entry points.

The routing guide's link text named the environment variable
FLET_ROUTE_URL_STRATEGY; it is FLET_WEB_ROUTE_URL_STRATEGY.
@ndonkoHenri
ndonkoHenri requested a balanced review from Copilot October 10, 2026 23:00

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Both reported regressions need automated coverage before approval.

1 open finding
What changed in this PR

Fixes hash-routed web apps returning the SPA with HTTP 200 for unknown paths.

Changes:

  • Return real 404 responses for missing hash-mode paths.
  • Accept string renderer and routing strategy values.
  • Correct routing documentation and changelog entries.
File Description
flet_static_files.py Adds enum conversion and hash-mode 404 handling.
navigation-and-routing.md Corrects the environment variable name.
CHANGELOG.md Documents both bug fixes.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Deploying flet-website-v2 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 00926c1
Status: ✅  Deploy successful!
Preview URL: https://d2d2fea1.flet-website-v2.pages.dev
Branch Preview URL: https://fix-hash-routing-404-2796.flet-website-v2.pages.dev

View logs

ndonkoHenri and others added 3 commits October 11, 2026 01:48
page.login(redirect_to_page=True) built the return URL as a path URL
from the WebSocket path: `/store` even with the hash route URL strategy,
where the route belongs in the fragment and an unknown path is a 404.
The WebSocket path also misses a prefix that a proxy strips (proxy_path)
and picks up the folder of a nested WebSocket endpoint.

FletApp now takes the route URL strategy that FletStaticFiles writes into
index.html, environment override included, and the page's base path,
computed like the served <base href> from proxy_path and the ASGI root
path. login() returns to `<base path>#<route>` with the hash strategy
and to `<base path><route>` otherwise.
Page.login() only said "Starts OAuth flow." It now describes the
authorization code flow and the saved-token restore, every argument and
the return value, with short comments on the steps in its body.

Connection's page_base_path, route_url_strategy and local_data_transport
get attribute docstrings instead of comments, and the route URL strategy
docs on Connection and FletApp no longer single out OAuth.
@FeodorFitsner
FeodorFitsner merged commit 7a88e93 into flet-1.1.0 Oct 11, 2026
2 of 113 checks passed
@FeodorFitsner
FeodorFitsner deleted the fix/hash-routing-404-2796 branch October 11, 2026 20:49
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