Skip to content

Inherit Starlette router dispatch - #16236

Open
Kludex wants to merge 1 commit into
fastapi:masterfrom
Kludex:use-starlette-router-resolve
Open

Inherit Starlette router dispatch#16236
Kludex wants to merge 1 commit into
fastapi:masterfrom
Kludex:use-starlette-router-resolve

Conversation

@Kludex

@Kludex Kludex commented Aug 22, 2026

Copy link
Copy Markdown
Member

Summary

Remove FastAPI's copy of Router.app() and express low-priority frontend routing through Starlette's existing default callback.

Starlette remains responsible for lifespan handling, normal route matching, partial matches, and slash redirects. FastAPI handles low-priority routes only after Starlette finds no regular route or redirect, then delegates to the original default application.

This works with FastAPI's minimum supported Starlette version, 0.46.0. Once Starlette PR #3339 is available, FastAPI inherits its trie-backed candidate selection without another routing change.

Benchmark

The benchmark compares FastAPI master with this PR while both use the same Starlette trie branch at 3bfc970. It dispatches requests directly through the public FastAPI ASGI application. Results are median microseconds per request across four processes, with 2,000 requests per round and seven rounds per process. Lower is better.

Routes First route Last route Missing route Method not allowed
10 12.4 -> 12.9 us 17.5 -> 12.8 us (1.4x) 17.8 -> 9.1 us (2.0x) 13.8 -> 9.6 us (1.4x)
100 12.6 -> 13.1 us 63.9 -> 13.0 us (4.9x) 106.7 -> 11.1 us (9.6x) 60.8 -> 9.6 us (6.4x)
500 12.3 -> 12.9 us 266.4 -> 12.8 us (20.7x) 503.1 -> 19.1 us (26.3x) 261.6 -> 9.5 us (27.6x)

The first route remains approximately 13 us. The improvement grows with route count for late matches, misses, and 405 Method Not Allowed responses. These gains apply when FastAPI is used with the trie-backed Starlette change.

Tests

  • scripts/test.sh -q - 3348 passed, 17 skipped, 5 xfailed
  • scripts/lint.sh
  • tests/test_frontend.py against Starlette 0.46.0 - 93 passed

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

@codspeed-hq

codspeed-hq Bot commented Aug 22, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing Kludex:use-starlette-router-resolve (4c60b99) with master (8e89896)1

Open in CodSpeed

Footnotes

  1. No successful run was found on master (c3f316b) during the generation of this report, so 8e89896 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Mukller Mukller left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verified locally on the PR branch (Python 3.13, editable install of the branch):

Behavioral parity probe (branch vs release 0.141.1) — four contracts, identical results:

case branch release
GET /path/ with only /path registered 307 → http://testserver/path same
GET /unknown 404 {"detail": "Not Found"} same
POST on GET-only path 405 + allow: GET same
/openapi.json 200 same

Targeted test files: test_router_redirect_slashes.py, test_router_prefix_with_template.py, test_empty_router.py, test_router_events.py, test_include_router_defaults_overrides.py, test_ws_router.py70/70 green.

Checked the wiring details:

  1. Starlette's Router.app sets scope["router"] = self just like the removed fastapi copy did — no loss for middleware/introspection that reads it.
  2. Match ordering is preserved: starlette's loop handles FULL/PARTIAL + its own redirect_slashes pass, then calls self.default — which now points at _handle_no_match, so fastapi's low-priority route logic runs at the same point in the sequence as before, and _default keeps the original 404 handler.
  3. Deleting ~40 lines of drifted copy in favor of upstream's dispatch means future starlette routing fixes flow through automatically instead of needing manual sync — clearly the right direction.

One non-blocking nit: self._default = self.default; self.default = self._handle_no_match captures the original default at __init__ time, so a subclass that overrides .default after super().__init__() would have its override bypassed (_handle_no_match calls the captured _default). Extremely unlikely to matter in practice, but worth a comment line if you want to guard it.

CI fully green. Approving.

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