用 HTTPX 异步客户端处理请求与流式响应

用 HTTPX 异步客户端处理请求与流式响应

来源:HTTPX 官方文档:Async Support。原页未单列个人作者或发布日期,归属 HTTPX 项目与文档贡献者。本文依据 HTTPX 官方 Async Support 文档翻译整理。该页面不固定 HTTPX 发行版本;截至 2026 年 10 月 9 日,官方仓库列出的最新版本为 0.28.1,项目元数据要求 Python 3.9 或更高版本。下方输出是源文示例,不是当前请求结果。

HTTPX 默认提供同步 API,也提供 AsyncClient。当应用本身运行在异步 Web 框架中,需要向外发出 HTTP 请求时,异步客户端可以在等待网络 I/O 的阶段让出执行机会。其实际性能取决于负载、连接复用和调度方式,不能只凭使用了 async 就保证快于线程。

术语边界:原文提到异步并发可支持 WebSocket 一类的长连接,这是对异步模型用途的背景说明;本页介绍的是 HTTP 请求和响应接口,不是 HTTPX 自带 WebSocket 客户端的使用教程。

一个共享 AsyncClient 复用连接池;普通请求读取响应后结束,stream 上下文退出会关闭响应,手动 stream=True 的响应必须由开发者最终调用 aclose,应用结束时再关闭客户端。
未完纪绘制:客户端、响应和连接的生命周期。非抓包或实际运行截图。

从一个完整的异步请求开始

发送异步请求需要创建 AsyncClient,并在请求方法前使用 await。以下是原文 AsyncIO 部分的完整基本程序:

import asyncio
import httpx

async def main():
    async with httpx.AsyncClient() as client:
        response = await client.get("https://www.example.com/")
        print(response)

asyncio.run(main())

async with 管理客户端的打开和关闭;await client.get(...) 等待请求完成。原文交互式演示展示了 <Response [200 OK]>,这是原文示例的输出,不代表示例站点在所有环境或时点都会返回 200。

如果想在控制台逐行尝试,原页建议使用支持顶层 async/await 的 IPython,或者 Python 3.9+ 的 python -m asyncio。普通脚本不能直接在顶层随意使用 await;应像上例一样放入异步函数,再交给事件循环运行。这一 Python 版本说明是原页针对交互式用法的条件,并非在此替 HTTPX 的全部发行版本声明支持范围。

哪些请求方法需要 await

异步客户端中的 get、options、head、post、put、patch、delete、request 和 send 都是异步方法,调用形式为 response = await client.get(...) 等。切换到异步客户端,并不意味着所有对象的所有操作都要加 await:例如 build_request 只是构造请求对象,下面的手动流式示例直接调用它。

复用客户端,才能复用连接池

优先用上下文管理器确定客户端的作用域:

async with httpx.AsyncClient() as client:
    # 在这个作用域内复用 client。
    response = await client.get("https://www.example.com/")

不要在高频循环的每一次迭代内部都创建一个新的 AsyncClient,否则很难获得连接池复用带来的收益。原文建议共享一个有明确作用域的客户端并在需要的地方传递它,或者使用一个全局客户端。后者也必须有明确的关闭时机,不能因为它放在全局就忽略资源回收。

需要显式管理时,关闭方法是异步的 await client.aclose()。以下写法是对原文简短关闭示意的编辑补全,使用 finally 表达异常时也应关闭的责任:

async def fetch_once():
    client = httpx.AsyncClient()
    try:
        return await client.get("https://www.example.com/")
    finally:
        await client.aclose()

单次请求通常直接用 async with 更清楚;长期运行的服务则应把共享客户端的创建和关闭放到应用生命周期中。

流式响应:边到达,边消费

AsyncClient.stream(method, url, ...) 返回异步上下文管理器。与先把完整响应体读入内存相比,流式读取可以逐块处理数据。以下将原文的片段整理为完整函数,示例仅打印各块长度:

import asyncio
import httpx

async def main():
    async with httpx.AsyncClient() as client:
        async with client.stream("GET", "https://www.example.com/") as response:
            response.raise_for_status()  # 编辑补充:先检查 HTTP 错误状态。
            async for chunk in response.aiter_bytes():
                print(len(chunk))

asyncio.run(main())

raise_for_status() 是编辑加入的错误检查,不是原页这个片段中的原始语句。每块数据的边界由流式读取决定,不能把“一块”当成“一条业务消息”或“一行文字”。需要按行处理时,使用相应的按行接口。

方法 用途
await response.aread() 在流式上下文内按条件一次性读取响应内容;它会放弃逐块处理所提供的内存边界。
response.aiter_bytes() 以字节块迭代响应内容。
response.aiter_text() 以解码后的文本片段迭代。
response.aiter_lines() 按文本行迭代。
response.aiter_raw() 迭代不经过内容解码的原始响应字节,例如不执行内容压缩的解码。
await response.aclose() 关闭响应;通常退出 stream 上下文时会自动完成。

上述迭代方法使用 async for 消费。不要混淆文本解码、内容压缩解码与业务协议解析:若需保留传输过来的压缩内容字节,aiter_raw 与经过内容解码的字节流有不同含义。

手动流式转发:关闭响应是调用者的责任

当响应需要移交给另一个流式端点时,把所有消费逻辑包在当前函数的 async with 内可能不方便。HTTPX 支持构造 Request,再调用 client.send(..., stream=True) 进入手动模式。原文使用 Starlette 展示:

import httpx
from starlette.background import BackgroundTask
from starlette.responses import StreamingResponse

client = httpx.AsyncClient()

async def home(request):
    req = client.build_request("GET", "https://www.example.com/")
    r = await client.send(req, stream=True)
    return StreamingResponse(
        r.aiter_text(),
        background=BackgroundTask(r.aclose),
    )

这里返回给框架的是尚待消费的响应流。后台任务安排了 r.aclose,避免只转发数据却一直占用上游连接。官方特别强调:使用手动模式时,开发者必须确保最终调用 Response.aclose();遗漏关闭会导致连接和其他资源泄漏。

适用边界:这段代码是响应转发模式示意,原文没有给出完整应用及路由注册,也没有展示全局客户端在应用停机时如何关闭。它没有完整处理上游状态码、响应头、超时、异常中断及下游取消,不应直接当作可投产的通用代理。若 send 返回响应后又在构造或移交响应时出错,也应安排关闭上游响应;需要结合实际框架生命周期处理所有退出路径。

示例把目标网址固定为公开示例域名,没有从用户输入拼接目标,也没有嵌入秘密。若扩展为用户可指定 URL 的转发接口,访问目标校验、内网访问限制和身份凭证隔离都是新增的实际审查边界;不能从这段固定网址示例推导出任意 URL 代理是安全的。

请求体也可以异步产生

使用异步客户端发送流式请求体时,需要提供异步字节生成器,而不是同步的字节生成器。原文给出的接口示意如下:

async def upload_bytes():
    ...  # 在这里 yield 字节内容。

await client.post(url, content=upload_bytes())

这个片段中的 ... 是省略号,url 和 client 也来自外围上下文,因此它不是一段可直接运行的上传程序。尤其是只有省略号、没有 yield 的 async def,本身只是协程函数,并不是异步生成器。

一个真正符合所需形状的最小数据生产者可以写成下面这样。这是编辑补充,只展示字节生成,不声明任何服务端接收结果:

async def upload_bytes():
    yield b"first chunk\n"
    yield b"second chunk\n"

在已有客户端作用域中,将它产生的异步迭代对象作为 content=upload_bytes() 传给目标上传接口。真实上传还需要按该接口规定设置方法、媒体类型、鉴权和失败处理;不要把客户端“能够逐块发送”误认为服务器一定支持相同协议约定。

显式传输层也要使用异步类型

如果需要自己实例化传输对象,应选择 httpx.AsyncHTTPTransport。原文给出的例子设置了 retries=1:

transport = httpx.AsyncHTTPTransport(retries=1)
async with httpx.AsyncClient(transport=transport) as client:
    response = await client.get("https://www.example.com/")

最后一行请求是编辑补全,以替代原文省略的操作。这里展示的是异步传输与客户端的组合方式;retries 不应被理解成所有 HTTP 错误状态或所有业务操作都能安全重试的保证。官方传输说明将这项重试限定在 ConnectError 或 ConnectTimeout;读写失败或 503 等状态需要另行处理。具体传输行为参见官方 Transports 文档。

AsyncIO、Trio 和 AnyIO

HTTPX 支持 AsyncIO 或 Trio,并自动识别当前环境,为套接字操作和并发原语选用相应后端。AsyncIO 是 Python 内置库,本文开头已给出 asyncio.run(main()) 的完整例子。

Trio 是另一个异步库,需要另外安装 trio 包。注意入口用 trio.run(main),传入函数本身:

import httpx
import trio

async def main():
    async with httpx.AsyncClient() as client:
        response = await client.get("https://www.example.com/")
        print(response)

trio.run(main)

AnyIO 为 AsyncIO 与 Trio 提供统一的并发接口,默认后端是 AsyncIO;原文的示例显式选择 Trio:

import httpx
import anyio

async def main():
    async with httpx.AsyncClient() as client:
        response = await client.get("https://www.example.com/")
        print(response)

anyio.run(main, backend="trio")

显式选择 Trio 时,相应后端依赖仍需可用。如果需求是直接调用 Python 的 ASGI 应用,原页将读者引向 ASGITransport 文档;这与对外发起网络请求是不同的传输配置,不在本文扩展演示。

来源与许可边界

本文涵盖 Async Support 页面介绍的异步请求、客户端关闭、响应流、请求流、传输实例、AsyncIO、Trio、AnyIO 与 ASGI 指引,并标明省略上下文、编辑补充和资源管理责任。HTTPX 项目 pyproject 元数据将项目代码许可标为 BSD-3-Clause;随稿 LICENSE-HTTPX.txt保留官方版权声明、条件和免责声明。Async Support 页面未单独说明文档正文许可;本文中文整理与原创配图另行取得发布授权。许可文件适用于项目代码,不据此宣称源文页面采用软件许可,也不表示 Encode OSS Ltd 对本文背书。

使用手动流式响应时必须确保最终关闭;共享客户端要有停机回收责任;请求体流必须是真正的异步字节生成器;代理示例缺少完整的状态、头、超时与异常策略。HTTPX 传输重试只覆盖连接错误和连接超时,不保证所有状态码或业务操作都能安全重试。异步模型的实际表现依应用负载与连接复用而定,本文不提供性能保证。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容