httpx auth: Basic, Bearer token and refresh on 401 (tested)

Use httpx.get(url, auth=("user", "pass")) for Basic auth. For a Bearer token, set the Authorization header on the client or write a small httpx.Auth subclass; its auth_flow can also refresh the token on a 401.

httpx takes credentials through auth=: a (user, password) tuple for Basic auth, httpx.DigestAuth for Digest, or your own httpx.Auth subclass for Bearer tokens. Measured with httpx 0.28.1 on Python 3.13.5 against a local http.server (srv.py) that accepts Basic ada:s3cret on /basic, Bearer t2 on /bearer, and issues t2 from POST /token.

1. httpx auth basic

import httpx
from srv import start
base = start()

r = httpx.get(base + "/basic")
print(r.status_code, r.headers["www-authenticate"])

r = httpx.get(base + "/basic", auth=("ada", "s3cret"))
print(r.status_code, r.json())

r = httpx.get(base + "/basic", auth=httpx.BasicAuth("ada", "wrong"))
print(r.status_code)
r.raise_for_status()

Output:

401 Basic realm="api"
200 {'auth': 'Basic YWRhOnMzY3JldA=='}
401
Traceback (most recent call last):
  File "/tmp/ha394/b1.py", line 13, in <module>
    r.raise_for_status()
    ~~~~~~~~~~~~~~~~~~^^
  File "/tmp/fx384/ve/lib/python3.13/site-packages/httpx/_models.py", line 829, in raise_for_status
    raise HTTPStatusError(message, request=request, response=self)
httpx.HTTPStatusError: Client error '401 Unauthorized' for url 'http://127.0.0.1:8963/basic'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401

A tuple is shorthand for httpx.BasicAuth. Wrong credentials are not an exception: you get a 401 response, and only raise_for_status() turns it into httpx.HTTPStatusError.

2. Client and per-request auth

import httpx
from srv import start
base = start()

with httpx.Client(base_url=base, auth=("ada", "s3cret")) as client:
    print(client.get("/basic").status_code)
    # per-request auth overrides the client's
    print(client.get("/basic", auth=("ada", "nope")).status_code)

Output:

200
401

auth= on the Client applies to every request; auth= on a single call replaces it for that call.

3. Bearer token: a small httpx.Auth class

import httpx
from srv import start
base = start()

class BearerAuth(httpx.Auth):
    def __init__(self, token):
        self.token = token
    def auth_flow(self, request):
        request.headers["Authorization"] = f"Bearer {self.token}"
        yield request

r = httpx.get(base + "/bearer", auth=BearerAuth("t2"))
print(r.status_code, r.json())

# a plain string is not accepted as auth
try:
    httpx.get(base + "/bearer", auth="Bearer t2")
except TypeError as e:
    print("TypeError:", e)

Output:

200 {'auth': 'Bearer t2'}
TypeError: Invalid "auth" argument: 'Bearer t2'

httpx has no built-in Bearer class, and a string is rejected with TypeError: Invalid "auth" argument. Either set headers={"Authorization": f"Bearer {token}"} on the client, or subclass httpx.Auth and set the header in auth_flow() as above.

4. Refresh an expired token on 401

import httpx
from srv import start
base = start()

class RefreshingAuth(httpx.Auth):
    requires_response_body = True
    def __init__(self, token):
        self.token = token
    def auth_flow(self, request):
        request.headers["Authorization"] = f"Bearer {self.token}"
        response = yield request
        if response.status_code == 401:
            print("401 with", self.token, "-> refreshing")
            refresh = yield httpx.Request("POST", base + "/token")
            self.token = refresh.json()["access_token"]
            request.headers["Authorization"] = f"Bearer {self.token}"
            yield request

with httpx.Client(auth=RefreshingAuth("expired")) as client:
    r = client.get(base + "/bearer")
    print(r.status_code, r.json())
    r = client.get(base + "/bearer")
    print(r.status_code, r.json())

Output:

401 with expired -> refreshing
200 {'auth': 'Bearer t2'}
200 {'auth': 'Bearer t2'}

auth_flow is a generator: each yield sends a request and receives its response, so the class can retry once with a fresh token. requires_response_body = True is needed to read refresh.json() inside the flow. The new token is stored on the instance, so the second call goes straight through.

5. httpx asyncclient auth

import asyncio, httpx
from srv import start
base = start()

async def main():
    async with httpx.AsyncClient(base_url=base, auth=("ada", "s3cret")) as client:
        r = await client.get("/basic")
        print(r.status_code, r.json())

asyncio.run(main())

Output:

200 {'auth': 'Basic YWRhOnMzY3JldA=='}

The same auth= values work with AsyncClient. A custom class whose auth_flow only yields requests (no blocking I/O) works for both sync and async clients.

Related: httpx headers, httpx params, httpx vs requests differences, httpx retries.

Frequently asked questions

How do I use basic auth with httpx?

Pass auth=('user', 'password') or auth=httpx.BasicAuth('user', 'password') to the request or the Client.

How do I send a Bearer token with httpx?

Set headers={'Authorization': f'Bearer {token}'} on the Client, or subclass httpx.Auth and set the header in auth_flow(). auth='Bearer x' raises TypeError.

Does httpx raise an error on 401 Unauthorized?

No. It returns the 401 response; call r.raise_for_status() to get httpx.HTTPStatusError.

How do I refresh an OAuth token automatically in httpx?

Write an httpx.Auth subclass whose auth_flow yields the request, checks for 401, yields a token request, updates the header and yields the request again.

Written with AI assistance. Measured on 2026-10-23 with httpx 0.28.1 on Python 3.13.5 against a local http.server that checks Basic and Bearer credentials. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; httpx is free.