Client middleware API
See original GitHub issueThere’s been quite a few PRs trying to take a stab at making the Client
/AsyncClient
extensible, mostly with a middleware approach whose idea seems to have originated in https://github.com/encode/httpx/pull/136#issuecomment-516085808 to help solve Digest Auth.
As mentioned in https://github.com/encode/httpx/pull/268#issuecomment-525347835 it might be useful to define and discuss in which direction we’d like to go.
Problem statement
A few requirements we may want to satisfy, gathered from looking at the various discussions:
- Support the simplest case: modify the request (before getting a response from the underlying middleware).
- Support middleware that may return an early response (e.g. a cache middleware), i.e. allow to skip calling the underlying middleware.
- Support middleware that need to make subsequent requests after getting the initial response (e.g. Redirect, Digest Auth).
- Allow users to write their own middleware, e.g. for particular authentication schemes.
- Allow users to assemble a middleware pipeline (with what level of control?).
None of the approaches attempt to solve 4) nor 5) — and that can definitely be dealt with in its own issue once this one is resolved. As a result, the scope of this issue is designing an internal API to solve 1), 2) and 3).
Existing implementation
A reminder of the features that HTTPX currently supports that may benefit from being refactored as as middleware:
- Basic authentication: add an
Authorization
header based on ausername
andpassword
. - “Custom authentication”: process the request via a
req -> req
callable. (I personally think this might be too lose — we could just require the callable to return anAuthorization
header — but let’s tighten the scope of this issue and keep things as they are.) - Redirect: automatically handle redirect responses by making request to the specified location.
These are currently implemented as part of the BaseClient
itself. Them cluttering the BaseClient
is, as far as I understand, the main reason for refactoring them into loosely-coupled middleware.
Proposed solutions
Here’s a summary of the various APIs that were proposed up to now and their respective “no-op middleware” for the sake of example:
#176 : process_request / process_response
class Middleware:
async def process_request(self, request: AsyncRequest) -> AsyncRequest:
return request
async def process_response(
self, request: AsyncRequest, response: AsyncResponse
) -> AsyncResponse:
return response
#267 : building on top of the dispatcher API
from httpx.base import AsyncDispatcher
class Middleware(AsyncDispatcher):
def __init__(self, next_dispatcher, **kwargs):
self.next_dispatcher = next_dispatcher
async def send(
self,
request: AsyncRequest,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> AsyncResponse:
return await self.next_dispatcher.send(request, verify=verify, cert=cert, timeout=timeout)
#268: callable middleware
class Middleware:
async def __call__(self, request: AsyncRequest, get_response: typing.Callable) -> AsyncResponse:
return await get_response(request)
I think the various PRs proved that all APIs tick 1), 2), and 3) as defined above, with various degrees of simplicity and boilerplate.
The callable middleware API seems to be the simplest one while offering enough flexibility. Making extra requests based on the initial response can be achieved by recursively calling the middleware, as in the RedirectMiddleware. As another PoC, a rough Digest-Auth implementation based on this snippet:
class DigestAuthMiddleware:
async def __call__(self, request, get_response):
response = await get_response(request)
# Did the server respond with 401 + WWW-Authenticate?
if self.requires_authentication(response):
authorization = self.build_authorization_header(request, response)
request.headers["Authorization"] = authorization
return await self(request, get_response)
return response
Issue Analytics
- State:
- Created 4 years ago
- Reactions:2
- Comments:8 (8 by maintainers)
Top GitHub Comments
Great job on this @florimondmanca!
I think #268 is a great first implementation of this, chances are we’ll start noticing pain points as we start implementing more middlewares but at this point in the project it’s something we need to go through.
I’ll repurpose the Digest auth and look forward to more features using middlewares 🎉
Super @yeraydiazdiaz, I’m excited about Digest Auth!