FastAPI 自定义 Response 实战:HTML、流式、文件与自定义响应类的完整实现
本文基于 FastAPI 官方文档《Custom Response – HTML, Stream, File, others》(本仓库德语版 docs/de/docs/advanced/custom-response.md)展开,讲解如何突破 FastAPI 默认的 JSON 响应限制:使用 response_class 声明 HTML、纯文本、重定向、流式与文件响应,并创建自定义 Response 子类(例如基于 orjson 的响应)。读完本文,你将掌握“让 FastAPI 返回非 JSON 内容,同时保持 OpenAPI 文档与交互式文档正确生成”的全部技术手段,并能结合 fastapi/responses.py 与 fastapi/routing.py 的源码理解每种机制的底层执行路径。
一、默认 JSON 响应:三种序列化路径
FastAPI 默认返回 JSON 响应。根据你在路径操作(Path Operation)中的声明方式,底层实际走三条不同的序列化路径:
- 声明了 Response 模型:FastAPI 直接使用 Pydantic 把数据序列化为 JSON 字节,不经过
jsonable_encoder,也不会走JSONResponse类——这是性能最高的路径。 - 未声明 Response 模型:FastAPI 使用
jsonable_encoder把返回对象转成 JSON 兼容类型,再把结果包装进JSONResponse。 - 声明了
response_class且该类的媒体类型是application/json(例如JSONResponse):返回的数据依然会先被你的 Pydanticresponse_model(在路径操作装饰器中声明)转换(并过滤),但随后不是由 Pydantic 序列化成 JSON 字节,而是先经jsonable_encoder转换,再交给JSONResponse类用 Python 标准 JSON 库序列化为字节。
性能结论(与原文档一致):如果你追求极致性能,使用 Response 模型,并且不要在路径操作装饰器中声明
response_class。对应示例见 docs_src/response_model/tutorial001_01_py310.py:
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
app = FastAPI()
@app.get("/items/")
def read_item():
return Item(name="Foo", description="A pretty description")
这种写法下,FastAPI 用 Pydantic 直接序列化到 JSON 字节,省去了中间环节。文档在 JSONResponse 一节的“技术细节”中同样强调:当声明了 Response 模型或返回类型注解时,FastAPI 会直接用它把数据序列化为 JSON 并返回带正确媒体类型的响应,绕过 JSONResponse 类——这是获得最佳性能的理想方式。
源码佐证
- fastapi/applications.py 中各路由装饰器(
get、post等)的response_class参数默认值均为Default(JSONResponse)(例如 L1188、L1323),default_response_class也是应用级参数(L353),这解释了为什么“不声明即 JSON”。 - fastapi/routing.py L379-L400 是
APIRoute对response_class的解析入口:若传入的是DefaultPlaceholder则取默认值,否则原样使用,并判断是否为 SSE 流(EventSourceResponse子类)。L686-L703 展示了显式StreamingResponse时“原始流式”分支如何把生成器直接作为content构造响应对象。
二、HTML 响应:用 response_class 声明而非直接返回
要从 FastAPI 直接返回 HTML,使用 HTMLResponse:
- 导入
HTMLResponse; - 把
HTMLResponse作为路径操作装饰器的response_class参数传入。
对应 docs_src/custom_response/tutorial002_py310.py:
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/items/", response_class=HTMLResponse)
async def read_items():
return """
<html>
<head>
<title>Some HTML in here</title>
</head>
<body>
<h1>Look ma! HTML!</h1>
</body>
</html>
"""
关键机制:response_class 不仅决定响应的实际类型,还用来定义响应的媒体类型(Media Type)。上例中 HTTP 头 Content-Type 会被设置为 text/html,并且这个媒体类型会如实记录在生成的 OpenAPI 文档中。这一点在交互式文档中有直接体现——/items/ 的 200 响应会显示 text/html 的 Accept 选项(原文档配图即 Swagger UI 中的该界面截图,见 docs/en/docs/img/tutorial/custom-response/image01.png)。
注意(原文档 note):如果你使用一个不带媒体类型的 Response 类,FastAPI 会认为你的响应没有内容,因此不会在生成的 OpenAPI 文档中记录该响应的格式。
三、直接返回 Response 对象:可行,但会丢失 OpenAPI 文档
如 返回 Response 对象(本仓库对应 docs_src/response_directly/)所述,你也可以在路径操作函数中直接构造并返回 Response(或其子类)对象来覆盖默认行为。上面 HTML 示例的直接返回版本(docs_src/custom_response/tutorial003_py310.py):
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/items/")
async def read_items():
html_content = """
<html>
<head>
<title>Some HTML in here</title>
</head>
<body>
<h1>Look ma! HTML!</h1>
</body>
</html>
"""
return HTMLResponse(content=html_content, status_code=200)
警告:从路径操作函数直接返回的
Response不会出现在 OpenAPI 文档中(例如Content-Type不会被记录),在自动生成的交互式文档中也不可见。
不过,实际的 Content-Type 头、状态码等仍然来自你返回的那个 Response 对象本身——运行时行为完全正确,只是文档层面“隐身”了。
四、在 OpenAPI 中声明媒体类型,同时覆盖实际 Response
如果你既想在函数内覆盖实际响应,又想让 OpenAPI 记录媒体类型,可以两者并用:声明 response_class 参数,同时返回一个 Response 对象。此时 response_class 只用于 OpenAPI 路径操作的文档生成,你返回的 Response 原样生效。
例如(docs_src/custom_response/tutorial004_py310.py):
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
def generate_html_response():
html_content = """
<html>
<head>
<title>Some HTML in here</title>
</head>
<body>
<h1>Look ma! HTML!</h1>
</body>
</html>
"""
return HTMLResponse(content=html_content, status_code=200)
@app.get("/items/", response_class=HTMLResponse)
async def read_items():
return generate_html_response()
这里 generate_html_response() 已经构造好一个 HTMLResponse 并返回。你在路径操作中返回它,就等于直接返回了一个 Response,覆盖了 FastAPI 的默认行为;但由于同时把 HTMLResponse 传给了 response_class,FastAPI 知道应在 OpenAPI 和交互式文档中把它记录为 text/html 的 HTML 响应。
五、可用的 Response 类型全览
以下是常用 Response 类型速览。你可以用它们返回几乎任何内容,也可以创建自定义子类。
技术细节(原文档 note):也可以写成
from starlette.responses import HTMLResponse。FastAPI 通过 fastapi/responses.py 把starlette.responses中的同名类再次导出,纯粹是开发者便利——从源码结构看,该文件几乎全是from starlette.responses import X as X # noqa形式的再导出(L5-L12:EventSourceResponse、FileResponse、HTMLResponse、JSONResponse、PlainTextResponse、RedirectResponse、Response、StreamingResponse),大部分可用 Response 直接来自 Starlette。
5.1 Response
所有 Response 类的基类,可以直接返回。接受的参数:
content——str或bytes;status_code—— 整数 HTTP 状态码;headers—— 字符串字典;media_type—— 指定媒体类型的str,例如"text/html"。
FastAPI(实际是 Starlette)会自动插入 Content-Length 头;还会基于 media_type 插入 Content-Type 头,并对文本类型追加字符集(charset)。示例(docs_src/response_directly/tutorial002_py310.py):
from fastapi import FastAPI
from fastapi.responses import Response
app = FastAPI()
@app.get("/response-direct/")
async def main():
return Response(
content="<html><body><h1>Sorry, no content here</h1></body></html>",
media_type="text/html",
)
5.2 HTMLResponse
接受文本或字节,返回 HTML 响应,即上文第二节的用法。
5.3 PlainTextResponse
接受文本或字节,返回纯文本响应(docs_src/custom_response/tutorial005_py310.py):
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse
app = FastAPI()
@app.get("/", response_class=PlainTextResponse)
async def main():
return "Hello World"
5.4 JSONResponse
接受任意数据,返回 application/json 编码的响应。这是 FastAPI 的默认响应,前文已详述其三种序列化路径与性能差异。
技术细节:如声明了 Response 模型或返回类型,FastAPI 会直接用它把数据序列化为 JSON,并返回带正确媒体类型的响应,而不再经过
JSONResponse类——这是最佳性能路径。
5.5 RedirectResponse
返回 HTTP 重定向,默认状态码 307(临时重定向)。
用法一:直接返回(docs_src/custom_response/tutorial006_py310.py):
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
app = FastAPI()
@app.get("/typer")
async def redirect_typer():
return RedirectResponse("https://typer.tiangolo.com")
用法二:作为 response_class 声明(docs_src/custom_response/tutorial006b_py310.py)。这样你可以直接从路径操作函数返回 URL 字符串,使用的状态码就是 RedirectResponse 的默认码 307:
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
app = FastAPI()
@app.get("/fastapi", response_class=RedirectResponse)
async def redirect_fastapi():
return "https://fastapi.tiangolo.com"
用法三:response_class 与 status_code 参数组合(docs_src/custom_response/tutorial006c_py310.py),例如把重定向改为 302:
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
app = FastAPI()
@app.get("/pydantic", response_class=RedirectResponse, status_code=302)
async def redirect_pydantic():
return "https://docs.pydantic.dev/"
5.6 StreamingResponse
接受一个异步生成器,或普通生成器/迭代器(带 yield 的函数),对响应体进行流式输出(docs_src/custom_response/tutorial007_py310.py):
import anyio
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def fake_video_streamer():
for i in range(10):
yield b"some fake video bytes"
await anyio.sleep(0)
@app.get("/")
async def main():
return StreamingResponse(fake_video_streamer())
技术细节:
async任务只有在到达await点时才可能被取消。如果生成器(带yield的函数)中没有await,它无法被正确取消,可能在取消请求发出后继续运行。上面这个小程序例没有必需的await,所以加入await anyio.sleep(0),让事件循环有机会处理取消信号。对流量大的或无限的流,这一点尤其重要。
提示:与其直接返回
StreamingResponse,官方更推荐采用 流式返回数据 的写法,它更方便,且在后台替你处理取消。如果要流式返回 JSON Lines,参考 流式返回 JSON Lines。
5.7 FileResponse
把文件异步流式传输为响应。它的实例化参数与其他 Response 不同:
path—— 要流式传输的文件路径;headers—— 要包含的自定义头(字典);media_type—— 指定媒体类型的字符串。若不设置,会依据文件名或路径推断媒体类型;filename—— 若设置,会写入响应的Content-Disposition头。
文件响应会携带相应的 Content-Length、Last-Modified 和 ETag 头。
用法一:直接返回(docs_src/custom_response/tutorial009_py310.py):
from fastapi import FastAPI
from fastapi.responses import FileResponse
some_file_path = "large-video-file.mp4"
app = FastAPI()
@app.get("/")
async def main():
return FileResponse(some_file_path)
用法二:response_class 声明(docs_src/custom_response/tutorial009b_py310.py),此时可直接从函数返回文件路径字符串:
from fastapi import FastAPI
from fastapi.responses import FileResponse
some_file_path = "large-video-file.mp4"
app = FastAPI()
@app.get("/", response_class=FileResponse)
async def main():
return some_file_path
六、自定义 Response 类:继承 Response 并重写 render()
你可以创建自己的 Response 类,继承自 Response 并使用它。例如想用 orjson 并附加一些设置——让响应返回缩进格式化的 JSON,用到 orjson 的 OPT_INDENT_2 选项。你可以创建一个 CustomORJSONResponse;核心工作是实现一个把内容序列化为 bytes 的 render(content) 方法(docs_src/custom_response/tutorial009c_py310.py):
from typing import Any
import orjson
from fastapi import FastAPI, Response
app = FastAPI()
class CustomORJSONResponse(Response):
media_type = "application/json"
def render(self, content: Any) -> bytes:
assert orjson is not None, "orjson must be installed"
return orjson.dumps(content, option=orjson.OPT_INDENT_2)
@app.get("/", response_class=CustomORJSONResponse)
async def main():
return {"message": "Hello World"}
默认情况下接口返回:
{"message": "Hello World"}
换成自定义响应类后返回:
{
"message": "Hello World"
}
当然,你会找到比“格式化 JSON”更有价值的用途(原文档原话,此处只是演示机制本身)。
orjson 响应 vs Response 模型
如果目标是性能,Response 模型大概率比 orjson 响应类更合适:
- 使用 Response 模型时,FastAPI 用 Pydantic 直接序列化到 JSON,没有中间步骤(例如不必先经过
jsonable_encoder); - 从源码结构看,Pydantic 底层使用的 Rust 序列化机制与
orjson同源,因此用 Response 模型天然已经拿到了最佳性能。
这一点在 fastapi/responses.py 中也有直接印证:内置的 ORJSONResponse(L78-L98)与 UJSONResponse(L48-L66)均被标记 @deprecated,弃用提示明确写道“FastAPI now serializes data directly to JSON bytes via Pydantic when a return type or response model is set, which is faster and doesn't need a custom response class”(当设置返回类型或响应模型时,FastAPI 现在通过 Pydantic 直接把数据序列化为 JSON 字节,更快且无需自定义响应类)。也就是说:官方已经把“自定义 JSON 响应类换性能”这条路收敛为“声明返回类型/Response 模型”。
七、应用级默认响应类:default_response_class
在创建 FastAPI 实例或 APIRouter 时,可以通过 default_response_class 参数指定默认使用的响应类(fastapi/applications.py L353、L1555 即为 FastAPI 与 APIRouter 的该参数声明)。例如让所有路径操作默认使用 HTMLResponse 而非 JSON(docs_src/custom_response/tutorial010_py310.py):
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI(default_response_class=HTMLResponse)
@app.get("/items/")
async def read_items():
return "<h1>Items</h1><p>This is a list of items.</p>"
提示:即使在应用级设置了默认值,你仍然可以在单个路径操作中用
response_class覆盖,用法与之前完全一致。优先级关系可以概括为:路径操作装饰器的response_class>APIRouter的default_response_class>FastAPI的default_response_class> 内置默认JSONResponse。
八、在 OpenAPI 中补充响应细节
除了 response_class,你还可以用 responses 参数在 OpenAPI 中声明媒体类型和更多响应细节,参见 OpenAPI 中附加 Responses(本仓库对应源码示例见 docs_src/additional_responses/)。
小结
| 场景 | 推荐做法 | 依据 |
|---|---|---|
| 普通 JSON API,追求性能 | 声明 Response 模型 / 返回类型注解,不声明 response_class |
Pydantic 直接序列化到 JSON 字节 |
| 返回 HTML / 纯文本且要进 OpenAPI | response_class=HTMLResponse 等 |
tutorial002_py310.py |
| 响应由业务逻辑构造 | 直接返回 Response 对象 |
tutorial003_py310.py |
| 既覆盖响应又要 OpenAPI 文档 | response_class + 直接返回 Response 并用 |
tutorial004_py310.py |
| 重定向 | RedirectResponse(307 默认,可配 status_code=302 等) |
tutorial006b_py310.py |
| 大流/无限流 | 优先用文档推荐的流式写法;生成器中保留 await 点以支持取消 |
tutorial007_py310.py |
| 文件下载 | FileResponse,自带 Content-Length/Last-Modified/ETag |
tutorial009_py310.py |
| 应用全局默认响应类型 | FastAPI(default_response_class=...) |
tutorial010_py310.py |
理解 fastapi/routing.py 中 response_class 的解析与分发(L379-L400、L686-L747),以及 fastapi/responses.py 的 Starlette 再导出结构,你就能在任何自定义场景下准确预判 FastAPI 会如何序列化、如何生成 OpenAPI、以及取消信号会在哪里被处理。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00