Skip to content

Commit cfe3d21

Browse files
bdracoDreamsorcererpre-commit-ci[bot]
authored
[PR #10978/df30c55 backport][3.12] Cookbook changes (#10995)
Co-authored-by: Sam Bull <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 1b5b0d9 commit cfe3d21

File tree

8 files changed

+363
-460
lines changed

8 files changed

+363
-460
lines changed

.mypy.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[mypy]
2-
files = aiohttp, examples
2+
files = aiohttp, docs/code, examples
33
check_untyped_defs = True
44
follow_imports_for_stubs = True
55
#disallow_any_decorated = True

docs/client_advanced.rst

Lines changed: 27 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -124,29 +124,33 @@ Client Middleware
124124
-----------------
125125

126126
The client supports middleware to intercept requests and responses. This can be
127-
useful for authentication, logging, request/response modification, and retries.
127+
useful for authentication, logging, request/response modification, retries etc.
128128

129-
For practical examples and common middleware patterns, see the :ref:`aiohttp-client-middleware-cookbook`.
129+
For more examples and common middleware patterns, see the :ref:`aiohttp-client-middleware-cookbook`.
130130

131-
Creating Middleware
132-
^^^^^^^^^^^^^^^^^^^
131+
Creating a middleware
132+
^^^^^^^^^^^^^^^^^^^^^
133133

134-
To create a middleware, define an async function (or callable class) that accepts a request
135-
and a handler function, and returns a response. Middleware must follow the
136-
:type:`ClientMiddlewareType` signature (see :ref:`aiohttp-client-reference` for details).
134+
To create a middleware, define an async function (or callable class) that accepts a request object
135+
and a handler function, and returns a response. Middlewares must follow the
136+
:type:`ClientMiddlewareType` signature::
137137

138-
Using Middleware
139-
^^^^^^^^^^^^^^^^
138+
async def auth_middleware(req: ClientRequest, handler: ClientHandlerType) -> ClientResponse:
139+
req.headers["Authorization"] = get_auth_header()
140+
return await handler(req)
141+
142+
Using Middlewares
143+
^^^^^^^^^^^^^^^^^
140144

141-
You can apply middleware to a client session or to individual requests::
145+
You can apply middlewares to a client session or to individual requests::
142146

143147
# Apply to all requests in a session
144148
async with ClientSession(middlewares=(my_middleware,)) as session:
145-
resp = await session.get('http://example.com')
149+
resp = await session.get("http://example.com")
146150

147151
# Apply to a specific request
148152
async with ClientSession() as session:
149-
resp = await session.get('http://example.com', middlewares=(my_middleware,))
153+
resp = await session.get("http://example.com", middlewares=(my_middleware,))
150154

151155
Middleware Chaining
152156
^^^^^^^^^^^^^^^^^^^
@@ -155,13 +159,14 @@ Multiple middlewares are applied in the order they are listed::
155159

156160
# Middlewares are applied in order: logging -> auth -> request
157161
async with ClientSession(middlewares=(logging_middleware, auth_middleware)) as session:
158-
resp = await session.get('http://example.com')
162+
async with session.get("http://example.com") as resp:
163+
...
159164

160-
A key aspect to understand about the flat middleware structure is that the execution flow follows this pattern:
165+
A key aspect to understand about the middleware sequence is that the execution flow follows this pattern:
161166

162167
1. The first middleware in the list is called first and executes its code before calling the handler
163-
2. The handler is the next middleware in the chain (or the actual request handler if there are no more middleware)
164-
3. When the handler returns a response, execution continues in the first middleware after the handler call
168+
2. The handler is the next middleware in the chain (or the request handler if there are no more middlewares)
169+
3. When the handler returns a response, execution continues from the last middleware right after the handler call
165170
4. This creates a nested "onion-like" pattern for execution
166171

167172
For example, with ``middlewares=(middleware1, middleware2)``, the execution order would be:
@@ -172,7 +177,12 @@ For example, with ``middlewares=(middleware1, middleware2)``, the execution orde
172177
4. Exit ``middleware2`` (post-response code)
173178
5. Exit ``middleware1`` (post-response code)
174179

175-
This flat structure means that middleware is applied on each retry attempt inside the client's retry loop, not just once before all retries. This allows middleware to modify requests freshly on each retry attempt.
180+
This flat structure means that a middleware is applied on each retry attempt inside the client's retry loop,
181+
not just once before all retries. This allows middleware to modify requests freshly on each retry attempt.
182+
183+
For example, if we had a retry middleware and a logging middleware, and we want every retried request to be
184+
logged separately, then we'd need to specify ``middlewares=(retry_mw, logging_mw)``. If we reversed the order
185+
to ``middlewares=(logging_mw, retry_mw)``, then we'd only log once regardless of how many retries are done.
176186

177187
.. note::
178188

@@ -181,157 +191,6 @@ This flat structure means that middleware is applied on each retry attempt insid
181191
like adding static headers, you can often use request parameters
182192
(e.g., ``headers``) or session configuration instead.
183193

184-
Common Middleware Patterns
185-
^^^^^^^^^^^^^^^^^^^^^^^^^^
186-
187-
.. _client-middleware-retry:
188-
189-
Authentication and Retry
190-
""""""""""""""""""""""""
191-
192-
There are two recommended approaches for implementing retry logic:
193-
194-
1. **For Loop Pattern (Simple Cases)**
195-
196-
Use a bounded ``for`` loop when the number of retry attempts is known and fixed::
197-
198-
import hashlib
199-
from aiohttp import ClientSession, ClientRequest, ClientResponse, ClientHandlerType
200-
201-
async def auth_retry_middleware(
202-
request: ClientRequest,
203-
handler: ClientHandlerType
204-
) -> ClientResponse:
205-
# Try up to 3 authentication methods
206-
for attempt in range(3):
207-
if attempt == 0:
208-
# First attempt: use API key
209-
request.headers["X-API-Key"] = "my-api-key"
210-
elif attempt == 1:
211-
# Second attempt: use Bearer token
212-
request.headers["Authorization"] = "Bearer fallback-token"
213-
else:
214-
# Third attempt: use hash-based signature
215-
secret_key = "my-secret-key"
216-
url_path = str(request.url.path)
217-
signature = hashlib.sha256(f"{url_path}{secret_key}".encode()).hexdigest()
218-
request.headers["X-Signature"] = signature
219-
220-
# Send the request
221-
response = await handler(request)
222-
223-
# If successful or not an auth error, return immediately
224-
if response.status != 401:
225-
return response
226-
227-
# Return the last response if all retries are exhausted
228-
return response
229-
230-
2. **While Loop Pattern (Complex Cases)**
231-
232-
For more complex scenarios, use a ``while`` loop with strict exit conditions::
233-
234-
import logging
235-
236-
_LOGGER = logging.getLogger(__name__)
237-
238-
class RetryMiddleware:
239-
def __init__(self, max_retries: int = 3):
240-
self.max_retries = max_retries
241-
242-
async def __call__(
243-
self,
244-
request: ClientRequest,
245-
handler: ClientHandlerType
246-
) -> ClientResponse:
247-
retry_count = 0
248-
249-
# Always have clear exit conditions
250-
while retry_count <= self.max_retries:
251-
# Send the request
252-
response = await handler(request)
253-
254-
# Exit conditions
255-
if 200 <= response.status < 400 or retry_count >= self.max_retries:
256-
return response
257-
258-
# Retry logic for different status codes
259-
if response.status in (401, 429, 500, 502, 503, 504):
260-
retry_count += 1
261-
_LOGGER.debug(f"Retrying request (attempt {retry_count}/{self.max_retries})")
262-
continue
263-
264-
# For any other status code, don't retry
265-
return response
266-
267-
# Safety return (should never reach here)
268-
return response
269-
270-
Request Modification
271-
""""""""""""""""""""
272-
273-
Modify request properties based on request content::
274-
275-
async def content_type_middleware(
276-
request: ClientRequest,
277-
handler: ClientHandlerType
278-
) -> ClientResponse:
279-
# Examine URL path to determine content-type
280-
if request.url.path.endswith('.json'):
281-
request.headers['Content-Type'] = 'application/json'
282-
elif request.url.path.endswith('.xml'):
283-
request.headers['Content-Type'] = 'application/xml'
284-
285-
# Add custom headers based on HTTP method
286-
if request.method == 'POST':
287-
request.headers['X-Request-ID'] = f"post-{id(request)}"
288-
289-
return await handler(request)
290-
291-
Avoiding Infinite Recursion
292-
^^^^^^^^^^^^^^^^^^^^^^^^^^^
293-
294-
.. warning::
295-
296-
Using the same session from within middleware can cause infinite recursion if
297-
the middleware makes HTTP requests using the same session that has the middleware
298-
applied. This is especially risky in token refresh middleware or retry logic.
299-
300-
When implementing retry or refresh logic, always use bounded loops
301-
(e.g., ``for _ in range(2):`` instead of ``while True:``) to prevent infinite recursion.
302-
303-
To avoid recursion when making requests inside middleware, use one of these approaches:
304-
305-
**Option 1:** Disable middleware for internal requests::
306-
307-
async def log_middleware(
308-
request: ClientRequest,
309-
handler: ClientHandlerType
310-
) -> ClientResponse:
311-
async with request.session.post(
312-
"https://logapi.example/log",
313-
json={"url": str(request.url)},
314-
middlewares=() # This prevents infinite recursion
315-
) as resp:
316-
pass
317-
318-
return await handler(request)
319-
320-
**Option 2:** Check request details to avoid recursive application::
321-
322-
async def log_middleware(
323-
request: ClientRequest,
324-
handler: ClientHandlerType
325-
) -> ClientResponse:
326-
if request.url.host != "logapi.example": # Avoid infinite recursion
327-
async with request.session.post(
328-
"https://logapi.example/log",
329-
json={"url": str(request.url)}
330-
) as resp:
331-
pass
332-
333-
return await handler(request)
334-
335194
Custom Cookies
336195
--------------
337196

0 commit comments

Comments
 (0)