Skip to content

refactor(middleware): migrate RateLimitMiddleware to ASGIMiddleware - #5005

Open
Kumzy wants to merge 4 commits into
mw/allowed-hostsfrom
mw/rate-limit
Open

refactor(middleware): migrate RateLimitMiddleware to ASGIMiddleware#5005
Kumzy wants to merge 4 commits into
mw/allowed-hostsfrom
mw/rate-limit

Conversation

@Kumzy

@Kumzy Kumzy commented Aug 18, 2026

Copy link
Copy Markdown
Member

Part of #4009 (Stacked PR to avoid conflicts in the changelog and whats-new-3 docs)

Moving RateLimitMiddleware from AbstractMiddleware to ASGIMiddleware.

The constructor takes keyword arguments now instead of an app and a RateLimitConfig object, with each argument defaulting to the matching config field. RateLimitConfig itself is unchanged and stays the way to configure this: its middleware property now returns a configured RateLimitMiddleware instance instead of a DefineMiddleware, so middleware=[config.middleware] keeps working as before.

Same breaking change as the other PRs in the series: exclude patterns now match the handler's path template at startup instead of the request path (documented in the changelog and whats-new-3).

One subtlety: scopes now filters by handler type at startup, and rate limiting must keep applying to mounted ASGI apps (that's what the ::mount cache key bucket is for). So the middleware declares scopes = (HTTP, ASGI) with a runtime scope-type guard, meaning mounts stay rate limited while websocket connections through them pass through, like before.

Added some tests, each verified to fail when its kwarg is dropped from the middleware property:

  • One that checks set_rate_limit_headers=False really disables the headers. It was not covered before.
  • One that checks the four custom header key settings are applied. Dropping them was undetected before.
  • One that checks mounted ASGI apps are really rate limited (429 on the second request). The existing mount test couldn't tell "own bucket" apart from "not wrapped at all".
  • One that checks excluding a route works with the route pattern like /user/{user_id:int}, not the full URL like /user/1. It was failing before.
  • Extended the custom identity test so a different identity gets its own quota, without this, dropping identifier_for_request from the property was undetected.

Also updated the test_stores example test that reached into app.middleware[0].kwargs["config"], which no longer exists now that the property returns an instance.


📚 Documentation preview 📚: https://litestar-org.github.io/litestar-docs-preview/5005

@Kumzy
Kumzy requested review from a team as code owners August 18, 2026 07:42
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.19%. Comparing base (0f1910c) to head (84420b9).

Files with missing lines Patch % Lines
litestar/middleware/rate_limit.py 88.88% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@                 Coverage Diff                  @@
##           mw/allowed-hosts    #5005      +/-   ##
====================================================
- Coverage             67.19%   67.19%   -0.01%     
====================================================
  Files                   293      293              
  Lines                 15392    15404      +12     
  Branches               1749     1750       +1     
====================================================
+ Hits                  10342    10350       +8     
- Misses                 4896     4900       +4     
  Partials                154      154              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

RateLimitMiddleware moves off the legacy AbstractMiddleware base onto
ASGIMiddleware, taking its settings as keyword arguments (each defaulted
to mirror the RateLimitConfig field) instead of an app and a config.
RateLimitConfig stays as the public interface: its middleware property
now returns a configured RateLimitMiddleware instance instead of a
DefineMiddleware, so middleware=[config.middleware] keeps working.

scopes is (HTTP, ASGI) with a runtime scope-type guard, keeping mounted
ASGI apps rate limited (their own ::mount bucket) as before while
websocket handlers are bypassed at startup. exclude patterns now match
the handler's path template at startup instead of the request path at
runtime, and excluded handlers bypass the middleware entirely, matching
the other migrated middleware.

Every wiring kwarg in the middleware property is covered by a
drop-detecting test; new tests cover set_rate_limit_headers=False,
custom header keys, mount rate limiting, template-based exclusion, and
per-identity quotas.
@Kumzy

Kumzy commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Also implemented the from_config to reflect the changes done in CSRFMiddleware

Comment thread docs/release-notes/changelog.rst Outdated
built-in middleware off the legacy bases.

Applications that configure rate limiting through
:class:`~litestar.middleware.rate_limit.RateLimitConfig` and its ``middleware``

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 don't think that's the right migration path.

middleware=[rate_limit_config.middleware]

should become middleware=[RateLimitMiddleware(...)]

Comment thread docs/release-notes/changelog.rst Outdated
Comment thread litestar/middleware/rate_limit.py Outdated
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
"""ASGI callable.
@classmethod
def from_config(cls, config: RateLimitConfig) -> RateLimitMiddleware:

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.

Don't really think we need this here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes I did not look enough and tried to apply the same thing without looking at the same use

@provinzkraut

Copy link
Copy Markdown
Member

Since this is one of the cases where the config becomes obsolete, we should deprecate the config.middleware property

Kumzy and others added 2 commits August 18, 2026 13:18
Co-authored-by: Janek Nouvertné <provinzkraut@posteo.de>
Per review: rate limiting is one of the cases where the config becomes
obsolete now that the middleware is directly constructible. The
middleware property emits a deprecation warning (removal in 4.0) and the
documented migration path is passing a RateLimitMiddleware instance to
the middleware list directly. The from_config classmethod is dropped
again, tests and docs examples construct the middleware directly, and
the changelog entry documents the deprecation instead of the property
round-trip.
@Kumzy

Kumzy commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Since this is one of the cases where the config becomes obsolete, we should deprecate the config.middleware property

I create a deprecation in v2 branch, or is it now for 4.X ? like handlers/base.py @provinzkraut

…tring

The runtime deprecation warning and the changelog entry carry the
deprecation; the docstring directive has no precedent in the codebase.
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.

2 participants