httpx 完全指南

httpx 是现代 Python HTTP 客户端,同时支持同步和异步,是 requests 库的异步升级替代品。 使用 Client 可以复用连接、共享配置(headers、cookies、超时等): httpx 本身不内置重试,推荐配合 tenacity: 每个 Client 维护自己的连接池,频繁创建/销毁会导致连接无法复用,性能差。应在应用启动时创建,贯穿整个生命周期(或通过 FastAPI 依赖注入共享)。 生产代码使用 AsyncClient 作为应用级单例:AsyncClient 内部维护连接池,每次请求新建实例会频繁建立 TCP 连接,应

分享

官方文档:https://www.python-httpx.org/
最后更新:2026-03-29


1. 基础概念

httpx 是什么

httpx 是现代 Python HTTP 客户端,同时支持同步和异步,是 requests 库的异步升级替代品。

特性 requests httpx
异步支持 原生支持(AsyncClient)
HTTP/2 支持(需安装 httpx[http2]
类型注解 较少 完整
连接池 有(Client 级别)
超时控制 简单 细粒度(连接/读/写/池)

安装

pip install httpx

# 可选功能
pip install httpx[http2]    # HTTP/2 支持
pip install httpx[brotli]   # Brotli 压缩支持

2. 基础使用

单次请求(不推荐用于生产)

import httpx

# 同步
response = httpx.get("https://httpbin.org/get")
response = httpx.post("https://httpbin.org/post", json={"key": "value"})

# 异步
import asyncio

async def main():
    response = await httpx.get("https://httpbin.org/get")  # 不推荐,每次创建新连接
asyncio.run(main())

Client / AsyncClient(推荐)

使用 Client 可以复用连接、共享配置(headers、cookies、超时等):

import httpx

# 同步客户端(上下文管理器自动关闭连接)
with httpx.Client(base_url="https://api.example.com") as client:
    response = client.get("/users")
    response = client.post("/users", json={"name": "Alice"})

# 异步客户端
import asyncio

async def main():
    async with httpx.AsyncClient(base_url="https://api.example.com") as client:
        response = await client.get("/users")
        data = response.json()

asyncio.run(main())

3. 请求参数

请求方法与参数

async with httpx.AsyncClient() as client:
    # GET 查询参数
    r = await client.get("/search", params={"q": "python", "page": 1})
    # 实际 URL: /search?q=python&page=1

    # POST JSON 请求体
    r = await client.post("/users", json={"name": "Alice", "age": 25})

    # POST 表单
    r = await client.post("/login", data={"username": "alice", "password": "secret"})

    # POST 文件上传
    with open("photo.jpg", "rb") as f:
        r = await client.post("/upload", files={"photo": f})

    # 多文件 + 表单混合
    r = await client.post("/upload", files={
        "photo": ("photo.jpg", open("photo.jpg", "rb"), "image/jpeg"),
    }, data={"description": "头像"})

    # 自定义请求头
    r = await client.get("/protected", headers={"Authorization": "Bearer token123"})

    # 发送 cookies
    r = await client.get("/profile", cookies={"session": "abc123"})

Response 对象

属性/方法 说明
r.status_code HTTP 状态码
r.headers 响应头(字典)
r.cookies 响应 Cookies
r.text 响应文本(自动检测编码)
r.content 响应二进制内容
r.json() 解析 JSON,返回字典
r.url 最终请求 URL(含重定向)
r.encoding 检测到的编码
r.raise_for_status() 4xx/5xx 时抛出 HTTPStatusError
r.is_success 状态码 2xx
r.is_redirect 状态码 3xx
r.elapsed 请求耗时(timedelta)
r = await client.get("/users")
r.raise_for_status()  # 非 2xx 时抛出异常
users = r.json()

4. Client 配置

Client 初始化参数

参数 类型 说明
base_url str 基础 URL,所有请求路径自动拼接
headers dict 默认请求头(每次请求都会携带)
cookies dict 默认 Cookies
timeout float / Timeout 超时设置
follow_redirects bool 是否自动跟随重定向,默认 False
verify bool / str SSL 验证,False 关闭,str 指定证书路径
proxy str 单一代理 URL,所有流量通过该代理路由
mounts dict 自定义传输挂载,可按 URL 前缀分别设置代理
auth tuple / Auth 认证(Basic Auth 等)
limits Limits 连接池限制
http2 bool 启用 HTTP/2
event_hooks dict 请求/响应钩子

超时配置

# 统一超时(秒)
client = httpx.AsyncClient(timeout=10.0)

# 细粒度超时
timeout = httpx.Timeout(
    connect=5.0,   # 建立连接的超时
    read=30.0,     # 读取响应的超时
    write=10.0,    # 发送请求的超时
    pool=5.0,      # 等待连接池空闲的超时
)
client = httpx.AsyncClient(timeout=timeout)

# 单次请求覆盖
r = await client.get("/slow", timeout=60.0)

# 禁用超时(不推荐)
r = await client.get("/stream", timeout=None)

连接池限制

limits = httpx.Limits(
    max_connections=100,      # 最大连接数
    max_keepalive_connections=20,  # 最大保活连接数
    keepalive_expiry=30,      # 保活连接过期时间(秒)
)
client = httpx.AsyncClient(limits=limits)

5. 认证

内置认证

# Basic Auth
client = httpx.AsyncClient(auth=("username", "password"))
r = await client.get("/protected")

# Bearer Token(两种写法)
client = httpx.AsyncClient(headers={"Authorization": "Bearer my-token"})

r = await client.get("/protected", auth=("user", "pass"))

自定义认证类

import httpx

class BearerAuth(httpx.Auth):
    def __init__(self, token: str):
        self.token = token

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

class JWTRefreshAuth(httpx.Auth):
    """Token 过期时自动刷新的认证"""
    def __init__(self, token: str, refresh_token: str):
        self.token = token
        self.refresh_token = refresh_token

    def auth_flow(self, request: httpx.Request):
        request.headers["Authorization"] = f"Bearer {self.token}"
        response = yield request  # 发送请求,获取响应

        if response.status_code == 401:
            # 刷新 token
            refresh_response = yield self._build_refresh_request()
            self.token = refresh_response.json()["access_token"]
            # 用新 token 重试原始请求
            request.headers["Authorization"] = f"Bearer {self.token}"
            yield request

    def _build_refresh_request(self):
        return httpx.Request(
            "POST",
            "https://api.example.com/auth/refresh",
            json={"refresh_token": self.refresh_token},
        )

client = httpx.AsyncClient(auth=BearerAuth("my-token"))

6. 事件钩子(Hook)

import httpx

async def log_request(request: httpx.Request):
    print(f">>> {request.method} {request.url}")

async def log_response(response: httpx.Response):
    await response.aread()  # 确保响应体已读取
    print(f"<<< {response.status_code} {response.url} ({response.elapsed.total_seconds():.3f}s)")

client = httpx.AsyncClient(
    event_hooks={
        "request": [log_request],
        "response": [log_response],
    }
)

7. 重试机制(配合 tenacity)

httpx 本身不内置重试,推荐配合 tenacity

pip install tenacity
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import httpx

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=1, max=10),
    retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
    reraise=True,
)
async def fetch_with_retry(client: httpx.AsyncClient, url: str) -> dict:
    r = await client.get(url, timeout=10)
    r.raise_for_status()
    return r.json()

8. 流式传输

流式响应(大文件下载)

async with httpx.AsyncClient() as client:
    async with client.stream("GET", "https://example.com/large-file.zip") as r:
        r.raise_for_status()
        with open("file.zip", "wb") as f:
            async for chunk in r.aiter_bytes(chunk_size=8192):
                f.write(chunk)

流式 JSON(如 SSE / NDJSON)

async with client.stream("GET", "/stream") as r:
    async for line in r.aiter_lines():
        if line.startswith("data:"):
            data = json.loads(line[5:])
            print(data)

9. 代理设置

# 全局代理(单一代理,0.28+ 新 API)
client = httpx.AsyncClient(proxy="http://127.0.0.1:7890")

# 按协议分别设置(使用 mounts)
client = httpx.AsyncClient(mounts={
    "http://": httpx.AsyncHTTPTransport(proxy="http://proxy:7890"),
    "https://": httpx.AsyncHTTPTransport(proxy="http://proxy:7890"),
})

# 忽略某些域名的代理(不走代理)
client = httpx.AsyncClient(mounts={
    "https://internal.company.com": None,
    "all://": httpx.AsyncHTTPTransport(proxy="http://proxy:7890"),
})

10. 常用代码段

封装通用 API 客户端

import httpx
from typing import Any

class APIClient:
    def __init__(self, base_url: str, token: str):
        self._client = httpx.AsyncClient(
            base_url=base_url,
            headers={"Authorization": f"Bearer {token}"},
            timeout=httpx.Timeout(connect=5.0, read=30.0),
        )

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        await self._client.aclose()

    async def get(self, path: str, **kwargs) -> Any:
        r = await self._client.get(path, **kwargs)
        r.raise_for_status()
        return r.json()

    async def post(self, path: str, **kwargs) -> Any:
        r = await self._client.post(path, **kwargs)
        r.raise_for_status()
        return r.json()

并发请求限速

import asyncio
import httpx

async def fetch_all(urls: list[str], concurrency: int = 10) -> list[dict]:
    sem = asyncio.Semaphore(concurrency)

    async def fetch(client: httpx.AsyncClient, url: str) -> dict:
        async with sem:
            r = await client.get(url, timeout=10)
            r.raise_for_status()
            return r.json()

    async with httpx.AsyncClient() as client:
        tasks = [fetch(client, url) for url in urls]
        return await asyncio.gather(*tasks, return_exceptions=True)

11. 最佳实践

始终使用上下文管理器或手动关闭

# 正确:with 语句自动关闭
async with httpx.AsyncClient() as client:
    ...

# 或手动关闭
client = httpx.AsyncClient()
try:
    ...
finally:
    await client.aclose()

不要为每次请求创建新的 Client

每个 Client 维护自己的连接池,频繁创建/销毁会导致连接无法复用,性能差。应在应用启动时创建,贯穿整个生命周期(或通过 FastAPI 依赖注入共享)。

raise_for_status() 放在响应后立即调用

r = await client.get(url)
r.raise_for_status()  # 早失败,避免用 None 或错误数据继续执行
data = r.json()

12. 踩坑与注意事项

不带 base_url 时,路径不要以 / 开头

# base_url="https://api.example.com/v1"
client.get("/users")   # 结果:https://users(错误!路径替换了整个 base)
client.get("users")    # 结果:https://api.example.com/v1/users(正确)

# 正确做法:base_url 以 / 结尾,路径不以 / 开头
client = httpx.AsyncClient(base_url="https://api.example.com/v1/")
client.get("users")    # https://api.example.com/v1/users

SSL 验证关闭仅限测试环境

# 开发/测试时忽略 SSL 证书验证
client = httpx.AsyncClient(verify=False)
# 生产环境必须开启(默认开启),或指定自定义 CA 证书
client = httpx.AsyncClient(verify="/path/to/ca-bundle.crt")

最佳实践

生产代码使用 AsyncClient 作为应用级单例AsyncClient 内部维护连接池,每次请求新建实例会频繁建立 TCP 连接,应在应用启动时创建一个实例并复用:

# FastAPI 集成示例
@asynccontextmanager
async def lifespan(app):
    app.state.http = httpx.AsyncClient(timeout=30.0)
    yield
    await app.state.http.aclose()

始终设置 timeout 参数:httpx 默认超时 5 秒,生产环境按业务 SLA 设置,区分连接超时和读取超时:

timeout = httpx.Timeout(connect=3.0, read=30.0, write=10.0, pool=5.0)
client = httpx.AsyncClient(timeout=timeout)

client.build_request() + client.send() 实现请求重试:分两步构造和发送请求,方便在重试逻辑中复用同一 Request 对象,而不是重新构造。

流式下载大文件用 stream 上下文管理器:避免将整个响应体加载到内存:

async with client.stream("GET", url) as resp:
    async for chunk in resp.aiter_bytes(chunk_size=8192):
        await file.write(chunk)

httpx.MockTransport 做单元测试:不依赖真实网络,替换传输层实现确定性测试:

transport = httpx.MockTransport(handler=lambda req: httpx.Response(200, json={"ok": True}))
client = httpx.AsyncClient(transport=transport)

常见陷阱

陷阱:AsyncClient 未关闭导致资源泄漏

现象: 程序退出时报 Unclosed client,或长时间运行后文件描述符耗尽。
原因: AsyncClient 持有连接池,需要显式关闭。直接实例化而不用 async with 或不调用 aclose() 时泄漏。
解决: 总是用 async with httpx.AsyncClient() as client,或在应用关闭时调用 await client.aclose()

陷阱:在同步函数中调用 AsyncClient

现象: 在非 async 函数中使用 AsyncClientRuntimeError: This event loop is already running 或 coroutine 未等待。
原因: AsyncClient 的方法是协程,必须在 async 上下文中 await
解决: 同步场景改用 httpx.Client(同步版本,API 相同):

with httpx.Client(timeout=10.0) as client:
    resp = client.get(url)

陷阱:重定向跟随导致意外发送认证头到第三方

现象: 携带 Authorization 头的请求被 301 重定向到其他域后,认证信息被发送到新域名。
原因: httpx 默认跟随重定向且不清除认证头(与 requests 不同)。
解决: 敏感请求设置 follow_redirects=False,或使用 trust_env=False 配合自定义 auth 参数精确控制。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog