Problem
We currently have an internal middleware API that was brought in via #295 and #268.
We should be looking at making that API (or a different form of it?) public so that users can extend the functionality of the client to fit their needs.
For example, a retry functionality (discussed in https://github.com/encode/httpx/issues/108 and drafted in https://github.com/encode/httpx/pull/134 then abandoned) could probably be implemented as a middleware (basic idea here: https://github.com/encode/httpx/pull/268#issuecomment-526909874).
Questions to be answered
request, get_response -> response. What's the client-side API going to look like? Probably something like…client = httpx.Client()
client.add_middleware(RetryMiddleware, max_retries=3)
IMO, yes: treat those as core middleware, and add custom middleware on top. It might depend on the use case, but we can start off with that.
@sethmlarson proposed an idea inspired by Pyramid here: https://github.com/encode/httpx/issues/295#issuecomment-526933309
This was originally mentioned in https://github.com/encode/httpx/issues/295#issuecomment-526933309, and would be used to store state worth remembering such as cookies or Alt-Svc headers for HTTP/3 (see https://github.com/encode/httpx/issues/275#issuecomment-531067555).
IMO the use case still needs to be refined for us to clearly see what needs to be done.
What needs to be done
This is a proposal. :)
All thoughts welcome!
Yes this would be a great help. Specially I'm looking for an usecase for supporting OAuth2. I want the middleware ability so that I can add logic for getting OAuth token and attach required bearer header to actual request.
@kesavkolla Actually, the auth parameter also accepts a request -> request callable to modify the request before sending it (this is not yet properly documented, see #343). Without knowing the details of OAuth2, here's a proof-of-concept example:
import httpx
class OAuth2:
def __init__(self, token: str):
self.token = token
def __call__(self, request):
request.headers["<oauth-header>"] = build_oauth_bearer_header(self.token)
return request
client = httpx.Client(auth=OAuth2(token="..."))
It's not just injecting the header. There can be more auth flow that need to be executed. If token is expired need to call an API to refresh token etc... I was looking something similar to https://github.com/requests/requests-oauthlib
Okay so, I took a look at what the OAuth2Session is doing, and I'm not sure the current middleware API enables that use case, and I don't even think it should.
The high-level API of requests-oauthlib requires the user to interact with a special kind of Requests session. But you wouldn't be able to interact with middleware directly, because once registered they're basically buried in the client's middleware stack.
So I think you'd have more luck subclassing Client/AsyncClient directly, or going one level down by subclassing the ConnectionPool dispatcher. (A dispatcher is what an HTTPX client uses to actually send requests.)
An example with ConnectionPool, along with a Starlette-based version of their overview Flask example (obviously not tested):
from httpx import ConnectionPool
class OAuth2Dispatch(ConnectionPool):
async def request(self, method, url, **kwargs):
# …Attach header and check for token expiry here…
return await super().request(method, url, **kwargs)
# Helper methods for the OAuth2 flow:
def authorization_url(self, ...): ...
async def fetch_token(self, ...): ...
from httpx_oauthlib import OAuth2Dispatch
from starlette.applications import Starlette
from starlette.responses import RedirectResponse, JSONResponse
from starlette.middleware.session import SessionMiddleware
app = Starlette()
app.add_middleware(SessionMiddleware)
# This information is obtained upon registration of a new GitHub
client_id = "<your client key>"
client_secret = "<your client secret>"
authorization_base_url = 'https://github.com/login/oauth/authorize'
token_url = 'https://github.com/login/oauth/access_token'
@app.route("/login")
async def login(request):
github = OAuth2Dispatch(client_id)
authorization_url, state = github.authorization_url(authorization_base_url)
# State is used to prevent CSRF, keep this for later.
request.session['oauth_state'] = state
return RedirectResponse(url=authorization_url)
@app.route("/callback")
async def callback(request):
github = OAuth2Dispatch(client_id, state=session['oauth_state'])
token = await github.fetch_token(token_url, client_secret=client_secret, authorization_response=request.url)
r = await github.request("GET", "https://api.github.com/user")
return JSONResponse(r.json())
@kesavkolla I think what you need is something like https://docs.authlib.org/en/latest/client/httpx.html
Stay tuned, Authlib HTTPX feature is not released yet, it will be released in v0.13.
Looking great @lepture! I see there's also a Starlette integration — wondering how much of it is Starlette vs plain ASGI? — looks like two nice components for implementing complex authentication schemes in async web apps.
If you'd like any feedback on those integrations, feel free to ping @tomchristie, @sethmlarson, @yeraydiazdiaz or myself on a PR or issue. :) I think @tomchristie might be interested in trying out the Starlette integration for HostedAPI? (See tweet)
@florimondmanca The Starlette integration is not async currently. It was implemented by https://github.com/lepture/authlib/pull/153. I'm working on Starlette integration now to get it async since I've implemented the HTTPX async OAuth clients already.
Basically, web frameworks integrations API are all the same. In your web routes:
def login_via_github(request):
redirect_uri = 'your-callback-uri'
return oauth.github.authorize_redirect(request, redirect_uri)
def authorize_github(request)
token = oauth.github.authorize_access_token(request)
The documentation for Starlette is not updated yet. But basically all framework integrations are the same with a little differences among each of them. So the actually documentation is https://docs.authlib.org/en/latest/client/frameworks.html
@florimondmanca @tomchristie @sethmlarson
Here is a demo for Starlette Google login with Authlib. It works with Authlib wip-starlette branch.
wondering how much of it is Starlette vs plain ASGI?
Oh, I just found that I only imported one thing from starlette:
from starlette.responses import RedirectResponse
That means I can actually create a plain ASGI client.
So, I'd still like to see this, yup!
I think we'd go with Client(middleware: typing.Sequence[Middleware] = None).
The middleware ought to wrap around the call into send_single_request, here... https://github.com/encode/httpx/blob/831e79f50a59cb7c2af549fc186951703497fe08/httpx/client.py#L416
I think a good tack onto a PR for this would be to start with a PR that only supports an optional single item of middleware. We can then update the PR to support multiple middleware items. (I'm keen that we keep a close eye on making sure we're keeping things as clean as possible here, and tackling it in two stages would help us review it as fully as possible.)
This will need some significant design work.
I think we should treat this as out-of-scope at this point in time, in order to get an API-stable 1.0 release squared away.
That's how I'm feeling about this, too. We need to make sure we can safely introduce a middleware/extension API as an addition-only change, but not sure it's 100% necessary for a solid 1.0 release.
Great. Going to close this as out-of-scope for now, for the purposes of good housekeeping.
Yes, we clearly will come back to it, but it's not something we should focus on right now.
Most helpful comment
@kesavkolla I think what you need is something like https://docs.authlib.org/en/latest/client/httpx.html
Stay tuned, Authlib HTTPX feature is not released yet, it will be released in v0.13.