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 13:08 UTC- Status
- passed
- Last passed
- 2026-10-15 13:08 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 38738874208
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
Typed httpx client parsing into pydantic v2 models, tested with respx
An async httpx client that retries 5xx and transport errors, validates responses into pydantic v2 models, and is tested offline with respx route mocks.
FastAPI endpoint with Pydantic v2 discriminated unions and Annotated validators
Accept a tagged union of payment methods with Field(discriminator), add Annotated validators, and test the exact 422 error shapes offline with TestClient.
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.