From 8d71878d3a94c19d497c48acd7608495367608ac Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Tue, 28 Apr 2026 16:46:32 +0700 Subject: [PATCH 1/5] docs: update examples.md --- docs/en/tutorial/middleware/examples.md | 84 +++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/docs/en/tutorial/middleware/examples.md b/docs/en/tutorial/middleware/examples.md index 7e66bc9..8026043 100644 --- a/docs/en/tutorial/middleware/examples.md +++ b/docs/en/tutorial/middleware/examples.md @@ -197,3 +197,87 @@ app = FastHTTP( - `ttl` — cache time-to-live in seconds - `max_size` — maximum number of entries (LRU eviction) - `cache_methods` — list of methods to cache (default `["GET"]`) + +## Session / Cookie persistence + +FastHTTP includes built-in `SessionMiddleware` that automatically captures +`Set-Cookie` response headers and injects them as `Cookie` headers into all +subsequent requests — including across separate `app.run()` calls. + +```python +from fasthttp import FastHTTP, SessionMiddleware +from fasthttp.response import Response + +session = SessionMiddleware() +app = FastHTTP(middleware=session) +``` + +### Login flow (sequential runs with tags) + +Because all routes in one `run()` execute in parallel, use `tags` to run +requests in order when one depends on cookies set by another: + +```python +from fasthttp import FastHTTP, SessionMiddleware +from fasthttp.response import Response + +session = SessionMiddleware() +app = FastHTTP(middleware=session) + + +@app.post( + url="https://api.example.com/login", + json={"username": "alice", "password": "secret"}, + tags=["auth"], +) +async def login(resp: Response) -> dict: + # SessionMiddleware captures Set-Cookie from this response automatically + return resp.json() + + +@app.get(url="https://api.example.com/profile", tags=["protected"]) +async def profile(resp: Response) -> dict: + # Cookie header injected by SessionMiddleware + return resp.json() + + +app.run(tags=["auth"]) # login — cookies saved in session.cookies +app.run(tags=["protected"]) # profile — Cookie header injected automatically +``` + +### Pre-seeding cookies + +Pass an initial cookie dict to skip a login step: + +```python +session = SessionMiddleware(cookies={"auth_token": "already-have-this"}) +app = FastHTTP(middleware=session) +``` + +### Manual cookie management + +```python +# inspect what is stored +print(session.get_cookies()) # {"session_id": "abc123", ...} + +# remove all cookies (e.g. logout) +session.clear() +``` + +### Combining with other middleware + +`SessionMiddleware` uses `__priority__ = -10`, so it runs before everything +else by default — cookies are injected before auth or logging middleware sees the request. + +```python +from fasthttp import FastHTTP, SessionMiddleware, CacheMiddleware + +app = FastHTTP( + middleware=SessionMiddleware() | CacheMiddleware(ttl=60) +) +``` + +!!! note "Parallel requests and session state" + Within a single `app.run()` call, all routes fire concurrently via + `asyncio.gather`. If request B needs a cookie set by request A, split + them into separate `app.run(tags=[...])` calls so A completes before B starts. From 9ddefd8d8262109aa7836af57d8770707593adb9 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Tue, 28 Apr 2026 16:46:34 +0700 Subject: [PATCH 2/5] docs: update examples.md --- docs/ru/tutorial/middleware/examples.md | 88 +++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/docs/ru/tutorial/middleware/examples.md b/docs/ru/tutorial/middleware/examples.md index 5ff89a4..da87f9b 100644 --- a/docs/ru/tutorial/middleware/examples.md +++ b/docs/ru/tutorial/middleware/examples.md @@ -197,3 +197,91 @@ app = FastHTTP( - `ttl` — время жизни кэша в секундах - `max_size` — максимальное количество записей (LRU-вытеснение) - `cache_methods` — список методов для кэширования (по умолчанию `["GET"]`) + +## Сессии / Персистентность кук + +FastHTTP включает встроенный `SessionMiddleware`, который автоматически +перехватывает заголовки `Set-Cookie` из ответов и подставляет их как +`Cookie`-заголовок во все последующие запросы — в том числе между +отдельными вызовами `app.run()`. + +```python +from fasthttp import FastHTTP, SessionMiddleware +from fasthttp.response import Response + +session = SessionMiddleware() +app = FastHTTP(middleware=session) +``` + +### Login flow (последовательные запросы через tags) + +Все маршруты внутри одного `run()` выполняются параллельно, поэтому +для цепочки запросов, где каждый следующий зависит от куки предыдущего, +используй `tags`: + +```python +from fasthttp import FastHTTP, SessionMiddleware +from fasthttp.response import Response + +session = SessionMiddleware() +app = FastHTTP(middleware=session) + + +@app.post( + url="https://api.example.com/login", + json={"username": "alice", "password": "secret"}, + tags=["auth"], +) +async def login(resp: Response) -> dict: + # SessionMiddleware автоматически захватывает Set-Cookie из этого ответа + return resp.json() + + +@app.get(url="https://api.example.com/profile", tags=["protected"]) +async def profile(resp: Response) -> dict: + # Cookie-заголовок подставляется SessionMiddleware автоматически + return resp.json() + + +app.run(tags=["auth"]) # login — куки сохраняются в session.cookies +app.run(tags=["protected"]) # profile — Cookie-заголовок подставляется автоматически +``` + +### Предзагрузка кук + +Передай начальный словарь кук чтобы пропустить шаг логина: + +```python +session = SessionMiddleware(cookies={"auth_token": "already-have-this"}) +app = FastHTTP(middleware=session) +``` + +### Ручное управление куками + +```python +# посмотреть что хранится +print(session.get_cookies()) # {"session_id": "abc123", ...} + +# удалить все куки (например, logout) +session.clear() +``` + +### Комбинирование с другим middleware + +`SessionMiddleware` использует `__priority__ = -10`, то есть запускается +раньше всех остальных middleware — куки подставляются до того, как auth +или logging middleware видят запрос. + +```python +from fasthttp import FastHTTP, SessionMiddleware, CacheMiddleware + +app = FastHTTP( + middleware=SessionMiddleware() | CacheMiddleware(ttl=60) +) +``` + +!!! note "Параллельные запросы и состояние сессии" + Внутри одного вызова `app.run()` все маршруты запускаются одновременно + через `asyncio.gather`. Если запрос B нуждается в куке, которую устанавливает + запрос A, разбей их на отдельные `app.run(tags=[...])` — так A завершится + до начала B. From 1df3ce04ce1be8104d130428c04d641b5dca1075 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Tue, 28 Apr 2026 16:46:48 +0700 Subject: [PATCH 3/5] feat: create session_middleware.py --- examples/middleware/session_middleware.py | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 examples/middleware/session_middleware.py diff --git a/examples/middleware/session_middleware.py b/examples/middleware/session_middleware.py new file mode 100644 index 0000000..75df5dd --- /dev/null +++ b/examples/middleware/session_middleware.py @@ -0,0 +1,50 @@ +from fasthttp import FastHTTP, SessionMiddleware +from fasthttp.response import Response + +session = SessionMiddleware() +app = FastHTTP(middleware=session) + +@app.get( + url="https://httpbin.org/cookies/set", + params={"session_token": "abc123", "user": "alice"}, + tags=["set-cookies"], +) +async def set_cookies(resp: Response) -> dict: + print(f"Status: {resp.status}") + print(f"Captured cookies: {session.cookies}") + return {"status": resp.status} + + +@app.get(url="https://httpbin.org/cookies", tags=["read-cookies"]) +async def read_cookies(resp: Response) -> dict: + data = resp.json() + print(f"Server sees cookies: {data.get('cookies')}") + return data + + +@app.get(url="https://httpbin.org/cookies", tags=["pre-seeded"]) +async def inspect(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + print("=== Setting cookies ===") + app.run(tags=["set-cookies"]) + + print("\n=== Reading cookies (injected by SessionMiddleware) ===") + app.run(tags=["read-cookies"]) + + print(f"\nStored cookies: {session.get_cookies()}") + + session.clear() + + pre_seeded = SessionMiddleware(cookies={"auth_token": "xyz789"}) + app2 = FastHTTP(middleware=pre_seeded) + + @app2.get(url="https://httpbin.org/cookies", tags=["pre-seeded"]) + async def inspect2(resp: Response) -> dict: + return resp.json() + + print("\n=== Pre-seeded session ===") + app2.run() + print(f"Cookies after run: {pre_seeded.get_cookies()}") From cb21f28223c6a94128e95cf27d879ccaf59cb015 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Tue, 28 Apr 2026 16:46:53 +0700 Subject: [PATCH 4/5] update fasthttp/__init__.py --- fasthttp/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fasthttp/__init__.py b/fasthttp/__init__.py index bd7bec3..262eb8f 100644 --- a/fasthttp/__init__.py +++ b/fasthttp/__init__.py @@ -2,7 +2,7 @@ from .__meta__ import __version__ from .app import FastHTTP from .dependencies import Depends -from .middleware import BaseMiddleware, CacheMiddleware, MiddlewareChain, MiddlewareManager +from .middleware import BaseMiddleware, CacheMiddleware, MiddlewareChain, MiddlewareManager, SessionMiddleware from .routing import Router __all__ = ( @@ -13,6 +13,7 @@ "MiddlewareChain", "MiddlewareManager", "Router", + "SessionMiddleware", "__version__", "status", ) From f2bdaac5cbdee95970f32579ba7541aae202ced4 Mon Sep 17 00:00:00 2001 From: NEFORCEO Date: Tue, 28 Apr 2026 16:47:02 +0700 Subject: [PATCH 5/5] update middleware.py --- fasthttp/middleware.py | 90 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/fasthttp/middleware.py b/fasthttp/middleware.py index 92ec8ef..6be7044 100644 --- a/fasthttp/middleware.py +++ b/fasthttp/middleware.py @@ -400,3 +400,93 @@ def get_stats(self) -> dict: "ttl": self.ttl, "methods": self.cache_methods, } + + +class SessionMiddleware(BaseMiddleware): + """ + Middleware for persisting cookies across requests and ``app.run()`` calls. + + Captures ``Set-Cookie`` headers from responses and injects them as + ``Cookie`` headers into subsequent requests. State survives between + separate ``app.run()`` calls as long as the same instance is used. + + Class attributes control priority and filtering — same as any + :class:`BaseMiddleware` subclass. + + Example: + ```python + from fasthttp import FastHTTP + from fasthttp.middleware import SessionMiddleware + + session = SessionMiddleware() + app = FastHTTP(middleware=session) + + # or chain with other middleware + app = FastHTTP(middleware=session | CacheMiddleware()) + + @app.post("https://example.com/login", json={"user": "x", "pass": "y"}) + async def login(resp: Response) -> dict: + return resp.json() + + @app.get("https://example.com/profile") + async def profile(resp: Response) -> dict: + return resp.json() + + app.run(tags=["auth"]) + app.run(tags=["protected"]) + ``` + """ + + __return_type__ = None + __priority__ = -10 + __methods__ = None + __enabled__ = True + + def __init__( + self, + cookies: Annotated[ + dict[str, str] | None, + Doc("Pre-seed cookies to inject from the first request. Optional."), + ] = None, + ) -> None: + self.cookies: dict[str, str] = dict(cookies) if cookies else {} + + def __repr__(self) -> str: + return f"" + + async def request( + self, + method: Annotated[str, Doc("HTTP method.")], + url: Annotated[str, Doc("Request URL.")], + kwargs: Annotated[dict[str, Any], Doc("Request kwargs passed to httpx.")], + ) -> dict[str, Any]: + """Inject stored cookies into outgoing request.""" + if self.cookies: + headers = dict(kwargs.get("headers") or {}) + headers["Cookie"] = "; ".join( + f"{k}={v}" for k, v in self.cookies.items() + ) + kwargs["headers"] = headers + return kwargs + + async def response( + self, + response: Annotated["Response", Doc("Wrapped response object.")], + ) -> "Response": + """Capture Set-Cookie headers and persist them.""" + raw = response.headers.get("set-cookie", "") + if raw: + for cookie_str in raw.split(","): + name_value = cookie_str.split(";")[0].strip() + if "=" in name_value: + k, v = name_value.split("=", 1) + self.cookies[k.strip()] = v.strip() + return response + + def clear(self) -> None: + """Clear all stored cookies.""" + self.cookies.clear() + + def get_cookies(self) -> dict[str, str]: + """Return copy of current cookie store.""" + return dict(self.cookies)