Skip to content

Commit df30c55

Browse files
Dreamsorcererpre-commit-ci[bot]bdraco
authored
Cookbook changes (#10978)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <[email protected]>
1 parent 452458a commit df30c55

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, tests
2+
files = aiohttp, docs/code, examples, tests
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
@@ -131,29 +131,33 @@ Client Middleware
131131
-----------------
132132

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

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

138-
Creating Middleware
139-
^^^^^^^^^^^^^^^^^^^
138+
Creating a middleware
139+
^^^^^^^^^^^^^^^^^^^^^
140140

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

145-
Using Middleware
146-
^^^^^^^^^^^^^^^^
145+
async def auth_middleware(req: ClientRequest, handler: ClientHandlerType) -> ClientResponse:
146+
req.headers["Authorization"] = get_auth_header()
147+
return await handler(req)
148+
149+
Using Middlewares
150+
^^^^^^^^^^^^^^^^^
147151

148-
You can apply middleware to a client session or to individual requests::
152+
You can apply middlewares to a client session or to individual requests::
149153

150154
# Apply to all requests in a session
151155
async with ClientSession(middlewares=(my_middleware,)) as session:
152-
resp = await session.get('http://example.com')
156+
resp = await session.get("http://example.com")
153157

154158
# Apply to a specific request
155159
async with ClientSession() as session:
156-
resp = await session.get('http://example.com', middlewares=(my_middleware,))
160+
resp = await session.get("http://example.com", middlewares=(my_middleware,))
157161

158162
Middleware Chaining
159163
^^^^^^^^^^^^^^^^^^^
@@ -162,13 +166,14 @@ Multiple middlewares are applied in the order they are listed::
162166

163167
# Middlewares are applied in order: logging -> auth -> request
164168
async with ClientSession(middlewares=(logging_middleware, auth_middleware)) as session:
165-
resp = await session.get('http://example.com')
169+
async with session.get("http://example.com") as resp:
170+
...
166171

167-
A key aspect to understand about the flat middleware structure is that the execution flow follows this pattern:
172+
A key aspect to understand about the middleware sequence is that the execution flow follows this pattern:
168173

169174
1. The first middleware in the list is called first and executes its code before calling the handler
170-
2. The handler is the next middleware in the chain (or the actual request handler if there are no more middleware)
171-
3. When the handler returns a response, execution continues in the first middleware after the handler call
175+
2. The handler is the next middleware in the chain (or the request handler if there are no more middlewares)
176+
3. When the handler returns a response, execution continues from the last middleware right after the handler call
172177
4. This creates a nested "onion-like" pattern for execution
173178

174179
For example, with ``middlewares=(middleware1, middleware2)``, the execution order would be:
@@ -179,7 +184,12 @@ For example, with ``middlewares=(middleware1, middleware2)``, the execution orde
179184
4. Exit ``middleware2`` (post-response code)
180185
5. Exit ``middleware1`` (post-response code)
181186

182-
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.
187+
This flat structure means that a middleware is applied on each retry attempt inside the client's retry loop,
188+
not just once before all retries. This allows middleware to modify requests freshly on each retry attempt.
189+
190+
For example, if we had a retry middleware and a logging middleware, and we want every retried request to be
191+
logged separately, then we'd need to specify ``middlewares=(retry_mw, logging_mw)``. If we reversed the order
192+
to ``middlewares=(logging_mw, retry_mw)``, then we'd only log once regardless of how many retries are done.
183193

184194
.. note::
185195

@@ -188,157 +198,6 @@ This flat structure means that middleware is applied on each retry attempt insid
188198
like adding static headers, you can often use request parameters
189199
(e.g., ``headers``) or session configuration instead.
190200

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

0 commit comments

Comments
 (0)