Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions docs/en/tutorial/middleware/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
88 changes: 88 additions & 0 deletions docs/ru/tutorial/middleware/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
50 changes: 50 additions & 0 deletions examples/middleware/session_middleware.py
Original file line number Diff line number Diff line change
@@ -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()}")
3 changes: 2 additions & 1 deletion fasthttp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = (
Expand All @@ -13,6 +13,7 @@
"MiddlewareChain",
"MiddlewareManager",
"Router",
"SessionMiddleware",
"__version__",
"status",
)
90 changes: 90 additions & 0 deletions fasthttp/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<SessionMiddleware cookies={list(self.cookies.keys())}>"

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)
Loading