httpx async API client + respx mocks: working versions (tested 2026-10-15)
A typed async API client on httpx with retries on 5xx and Pydantic parsing, tested offline with respx route mocks.
passed3 passed · 0 failed2026-10-15 11:36 UTC- Status
- passed
- Last passed
- 2026-10-15 11:36 UTC
- Tests
- 3 / 3
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38728226240
The code
Exactly the files that passed. Your agent can pull them with get_recipe("httpx-respx-api-client") (install), or see them on GitHub.
httpx==0.28.1 respx==0.23.1 pydantic==2.14.1 pytest-asyncio==1.4.0 pytest==9.1.1
import httpx
from pydantic import BaseModel
class Repo(BaseModel):
id: int
full_name: str
stargazers_count: int
class ApiClient:
def __init__(self, base_url: str = "https://api.example.com", retries: int = 2):
self.http = httpx.AsyncClient(base_url=base_url, timeout=10)
self.retries = retries
async def get_repo(self, owner: str, name: str) -> Repo:
for attempt in range(self.retries + 1):
r = await self.http.get(f"/repos/{owner}/{name}")
if r.status_code < 500 or attempt == self.retries:
break
r.raise_for_status()
return Repo.model_validate(r.json())
async def aclose(self):
await self.http.aclose()
import httpx, pytest, respx
from client import ApiClient
@pytest.mark.asyncio
@respx.mock
async def test_parses_repo():
respx.get("https://api.example.com/repos/a/b").mock(return_value=httpx.Response(200, json={"id": 1, "full_name": "a/b", "stargazers_count": 5, "extra": True}))
c = ApiClient()
repo = await c.get_repo("a", "b")
assert repo.full_name == "a/b" and repo.stargazers_count == 5
await c.aclose()
@pytest.mark.asyncio
@respx.mock
async def test_retries_then_succeeds():
route = respx.get("https://api.example.com/repos/a/b").mock(side_effect=[httpx.Response(503), httpx.Response(200, json={"id": 1, "full_name": "a/b", "stargazers_count": 0})])
c = ApiClient()
await c.get_repo("a", "b")
assert route.call_count == 2
await c.aclose()
@pytest.mark.asyncio
@respx.mock
async def test_404_raises():
respx.get("https://api.example.com/repos/a/x").mock(return_value=httpx.Response(404))
c = ApiClient()
with pytest.raises(httpx.HTTPStatusError):
await c.get_repo("a", "x")
await c.aclose()
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
httpx + tenacity async retries honoring Retry-After, tested with respx
Wrap async httpx calls in tenacity AsyncRetrying that retries 429 and 5xx, honors Retry-After, and patches the wait to zero in tests with respx mocks.
httpx client + tenacity retries with backoff, tested with respx
Retry transient HTTP failures (5xx, 429, timeouts) with tenacity around an httpx client, honoring a max attempt count, tested offline with respx.
httpx OAuth2 client-credentials client with cached tokens and Link pagination
A sync httpx client that fetches and caches an OAuth2 client-credentials token, refreshes it on 401, and follows Link-header pagination, tested offline with respx routes.
OpenAI Python SDK structured outputs, tested with respx
Call the OpenAI Python SDK and validate JSON output with Pydantic, testing success, malformed output and API errors offline by mocking HTTP with respx.