Skip to content

feat(limiter): rate limiting on limits directly, slowapi removed - #22

Merged
JsNcAr merged 2 commits into
mainfrom
feat/limits-rate-limiter
Oct 14, 2026
Merged

JsNcAr merged 2 commits into
mainfrom
feat/limits-rate-limiter

Conversation

@JsNcAr

@JsNcAr JsNcAr commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Last of the migrations: removes the one thing standing between this code base and Python 3.16. slowapi 0.1.10 calls asyncio.iscoroutinefunction, removed in 3.16; upstream has had fixes open since 2025 (laurentS/slowapi#246, #264, #291) and none merged.

Branched from chore/sqlalchemy-2-1 (#21) because both touch poetry.lock and the changelog; merge #21 first.

Changes

  • limiter.py is rebuilt on limits 5.8, the library slowapi wraps and the project already had: MemoryStorage, FixedWindowRateLimiter, parse_many. Same configuration syntax (RATE_LIMIT_LOGIN=60/minute), same fixed-window behaviour, same per-IP keys.
  • A limited route declares dependencies=[Depends(limiter.limit(RATE_LIMIT_LOGIN))]. The request and response parameters the routes carried only for slowapi are gone; the dependency writes the X-RateLimit headers onto FastAPI's response object and raises RateLimitExceeded, mapped by main.py to the same 429 body and Retry-After.
  • Two small improvements that came with the rewrite: the 429 also carries the X-RateLimit headers (with Remaining: 0), and X-RateLimit-Reset is a whole number of seconds. Counters are keyed by method and route path template.
  • pyproject.toml: slowapi replaced by limits (>=5.8.0,<6.0); the lock loses only slowapi.
  • pytest.ini: the slowapi warning filter is removed. The suite now runs with no filter at all.
  • Known Issues: the slowapi entry is removed, which also empties the Dependencies section; the per-process entry's fix now names the limits storages.
  • Tests: test_limiter.py covers the handler, the headers, the 429 with its headers, per-route counters and the disabled limiter on a throwaway app without a database. test_rate_limit.py (database-backed) is unchanged in substance and still asserts Retry-After: 60 and the remaining budget.

Behaviour kept on purpose

Retry-After is still the window length (60 for a per-minute limit), not the seconds left in the window. Changing that is a client-visible contract change; the X-RateLimit-Reset header already gives the exact reset time.

Verification, local on Python 3.15.0 with PostgreSQL 17

black, flake8, mypy clean; importing the app with -W error clean; full suite, 140 tests, passed with -W error (every warning an error, nothing filtered).

🤖 Generated with Claude Code

JsNcAr and others added 2 commits October 14, 2026 11:25
The <2.1 constraint existed because 2.1 had no wheels, then because 2.1 is a
new minor. SQLModel 0.0.48 added 2.1 support, and 2.1 is the first SQLAlchemy
with a Python 3.15 wheel, so 2.0 ran as pure Python since the move to 3.15.
Declared as sqlalchemy[asyncio]: 2.1 installs greenlet only through that
extra. The lock changes one version; greenlet stays at 3.5.6.

Checked against the 2.1 migration guide: asyncpg is named in the URL (the
psycopg default does not apply), no named types, unconditional autoflush
is harmless here. Full suite passes on 3.15 with SADeprecationWarning,
SAWarning and SQLModel DeprecationWarnings as errors; lint, mypy and
pip-audit clean. Removes the Known Issues entry about the pure wheel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
slowapi 0.1.10 calls asyncio.iscoroutinefunction, removed in Python 3.16,
and the fix has sat in open upstream pull requests since 2025. The limiter
is now built on limits (the library slowapi wraps, already installed):
MemoryStorage, FixedWindowRateLimiter, parse_many, so the configuration
syntax and the fixed-window behaviour are unchanged.

A limited route declares Depends(limiter.limit(...)) instead of a decorator
plus request and response parameters it never used; the dependency counts
the request, writes X-RateLimit-Limit, -Remaining and -Reset onto the
response and raises RateLimitExceeded, which main.py maps to the same 429
body and Retry-After as before. New: the 429 also carries the X-RateLimit
headers, X-RateLimit-Reset is a whole number of seconds, and counters are
keyed by method and route path.

The pytest.ini filter for slowapi's warning is gone: the suite passes with
-W error. Tests cover the handler, the headers, the 429, per-route counters
and the disabled limiter without a database.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@JsNcAr
JsNcAr merged commit 8726621 into main Oct 14, 2026
2 checks passed
@JsNcAr
JsNcAr deleted the feat/limits-rate-limiter branch October 14, 2026 16:45
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.

1 participant