首页
/ FastAPI 自定义 Response 实战:HTML、流式、文件与自定义响应类的完整实现

FastAPI 自定义 Response 实战:HTML、流式、文件与自定义响应类的完整实现

2026-09-06 19:16:58作者:凌朦慧Richard

本文基于 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.pyfastapi/routing.py 的源码理解每种机制的底层执行路径。

一、默认 JSON 响应:三种序列化路径

FastAPI 默认返回 JSON 响应。根据你在路径操作(Path Operation)中的声明方式,底层实际走三条不同的序列化路径:

  1. 声明了 Response 模型:FastAPI 直接使用 Pydantic 把数据序列化为 JSON 字节,不经过 jsonable_encoder,也不会走 JSONResponse 类——这是性能最高的路径。
  2. 未声明 Response 模型:FastAPI 使用 jsonable_encoder 把返回对象转成 JSON 兼容类型,再把结果包装进 JSONResponse
  3. 声明了 response_class 且该类的媒体类型是 application/json(例如 JSONResponse):返回的数据依然会先被你的 Pydantic response_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 中各路由装饰器(getpost 等)的 response_class 参数默认值均为 Default(JSONResponse)(例如 L1188、L1323),default_response_class 也是应用级参数(L353),这解释了为什么“不声明即 JSON”。
  • fastapi/routing.py L379-L400 是 APIRouteresponse_class 的解析入口:若传入的是 DefaultPlaceholder 则取默认值,否则原样使用,并判断是否为 SSE 流(EventSourceResponse 子类)。L686-L703 展示了显式 StreamingResponse 时“原始流式”分支如何把生成器直接作为 content 构造响应对象。

二、HTML 响应:用 response_class 声明而非直接返回

要从 FastAPI 直接返回 HTML,使用 HTMLResponse

  1. 导入 HTMLResponse
  2. 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/htmlAccept 选项(原文档配图即 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.pystarlette.responses 中的同名类再次导出,纯粹是开发者便利——从源码结构看,该文件几乎全是 from starlette.responses import X as X # noqa 形式的再导出(L5-L12:EventSourceResponseFileResponseHTMLResponseJSONResponsePlainTextResponseRedirectResponseResponseStreamingResponse),大部分可用 Response 直接来自 Starlette。

5.1 Response

所有 Response 类的基类,可以直接返回。接受的参数:

  • content —— strbytes
  • 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_classstatus_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-LengthLast-ModifiedETag 头。

用法一:直接返回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;核心工作是实现一个把内容序列化为 bytesrender(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 即为 FastAPIAPIRouter 的该参数声明)。例如让所有路径操作默认使用 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 > APIRouterdefault_response_class > FastAPIdefault_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.pyresponse_class 的解析与分发(L379-L400、L686-L747),以及 fastapi/responses.py 的 Starlette 再导出结构,你就能在任何自定义场景下准确预判 FastAPI 会如何序列化、如何生成 OpenAPI、以及取消信号会在哪里被处理。

登录后查看全文
热门项目推荐
相关项目推荐