Skip to content

Commit ce01fae

Browse files
authored
feat: switch default HTTP client to httpx2 and accept a custom http_client (#731)
1 parent 7d3a7f1 commit ce01fae

11 files changed

Lines changed: 1267 additions & 72 deletions

‎README.md‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,81 @@ except RateLimitExceededError as e:
138138

139139
The client automatically retries requests up to 3 times (configurable via the `max_retries` request option) on 429 and 5xx responses, timeouts, and connection errors, using exponential backoff with jitter and honoring `Retry-After`. The SDK attaches an auto-generated `Idempotency-Key` (UUID v4) to every `POST` request and reuses the same key across its internal retries.
140140

141+
## HTTP Backends
142+
143+
The SDK sends requests through [`httpx2`](https://pypi.org/project/httpx2/) by default. Pass your own configured client as `http_client` to control proxies, TLS, connection limits, or transports:
144+
145+
```python
146+
import httpx2
147+
from workos import AsyncWorkOSClient, WorkOSClient
148+
149+
client = WorkOSClient(
150+
api_key="sk_...",
151+
http_client=httpx2.Client(proxy="http://proxy.internal:3128", verify="/etc/ssl/corp.pem"),
152+
)
153+
154+
async_client = AsyncWorkOSClient(
155+
api_key="sk_...",
156+
http_client=httpx2.AsyncClient(limits=httpx2.Limits(max_connections=20)),
157+
)
158+
```
159+
160+
`httpx` 0.28 clients are accepted as well; the two libraries share an API. The SDK closes only clients it created. A client you pass in stays open after `client.close()`, so close it yourself when you are done with it.
161+
162+
### Custom backends
163+
164+
Any object implementing `workos.HTTPBackend` (or `workos.AsyncHTTPBackend` for the async client) can be passed as `http_client`. The SDK hands the backend a fully built URL, the final headers, an optional `bytes` body, and a timeout in seconds, and expects a `workos.HTTPResponse` back. Raise `workos.TransportTimeout`, `workos.TransportConnectError`, or `workos.TransportError` for network failures so the SDK's retry logic can handle them. The `headers` mapping on the response must be case-insensitive.
165+
166+
The following `aiohttp` adapter is an example, not a supported part of the SDK:
167+
168+
```python
169+
import asyncio
170+
171+
import aiohttp
172+
import yarl
173+
174+
from workos import (
175+
AsyncWorkOSClient,
176+
HTTPResponse,
177+
TransportConnectError,
178+
TransportError,
179+
TransportTimeout,
180+
)
181+
182+
183+
class AiohttpBackend:
184+
def __init__(self, session: aiohttp.ClientSession) -> None:
185+
self._session = session
186+
187+
async def request(self, method, url, *, headers, content, timeout) -> HTTPResponse:
188+
try:
189+
async with self._session.request(
190+
method,
191+
yarl.URL(url, encoded=True), # keep the SDK's percent-encoding intact
192+
headers=headers,
193+
data=content,
194+
timeout=aiohttp.ClientTimeout(total=timeout),
195+
allow_redirects=True,
196+
) as resp:
197+
body = await resp.read()
198+
return HTTPResponse(resp.status, resp.headers, body, method, str(resp.url))
199+
except (asyncio.TimeoutError, aiohttp.ServerTimeoutError) as exc: # before connection errors
200+
raise TransportTimeout(str(exc)) from exc
201+
except aiohttp.ClientConnectionError as exc:
202+
raise TransportConnectError(str(exc)) from exc
203+
except aiohttp.ClientError as exc:
204+
raise TransportError(str(exc)) from exc
205+
206+
async def close(self) -> None:
207+
await self._session.close()
208+
209+
210+
async def main() -> None:
211+
async with aiohttp.ClientSession() as session:
212+
client = AsyncWorkOSClient(api_key="sk_...", http_client=AiohttpBackend(session))
213+
page = await client.organizations.list_organizations()
214+
```
215+
141216
## Per-Request Options
142217

143218
Every API method accepts `request_options` for per-call overrides (local helpers such as webhook/Actions signature verification and PKCE utilities do not make HTTP calls and don't take `request_options`):

‎pyproject.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ requires-python = ">=3.10"
99

1010
dependencies = [
1111
"cryptography~=50.0",
12-
"httpx~=0.28",
12+
"httpx2~=2.13",
1313
"pyjwt~=2.12",
1414
"typing_extensions~=4.0; python_version < '3.11'",
1515
]
@@ -31,10 +31,10 @@ test = [
3131
"pytest~=9.0",
3232
"pytest-asyncio~=1.3",
3333
"pytest-cov~=7.1",
34-
"pytest-httpx~=0.36",
34+
"httpx~=0.28",
3535
]
3636
lint = ["ruff~=0.15"]
37-
type_check = ["pyright~=1.1"]
37+
type_check = ["pyright~=1.1", "httpx~=0.28"]
3838
nox = ["nox~=2026.2", "nox-uv~=0.7"]
3939
docs = ["pdoc>=14", "pydoc-markdown>=4", "black~=26.3"]
4040

‎src/workos/__init__.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
ServerError,
1515
UnprocessableEntityError,
1616
)
17+
from ._http import (
18+
AsyncHTTPBackend,
19+
HTTPBackend,
20+
HTTPResponse,
21+
TransportConnectError,
22+
TransportError,
23+
TransportTimeout,
24+
)
1725
from ._pagination import AsyncPage, ListMetadata, SyncPage
1826
from .public_client import create_public_client
1927
from ._types import NOT_GIVEN, NotGiven, RequestOptions
@@ -37,4 +45,10 @@
3745
"NOT_GIVEN",
3846
"NotGiven",
3947
"create_public_client",
48+
"HTTPBackend",
49+
"AsyncHTTPBackend",
50+
"HTTPResponse",
51+
"TransportError",
52+
"TransportTimeout",
53+
"TransportConnectError",
4054
]

0 commit comments

Comments
 (0)