FastAPI系列06:FastAPI响应(Response)

FastAPI响应(Response)


在"FastAPI系列05:FastAPI请求(Request)"一节中我们详细了解了FastAPI程序对请求参数的校验和处理,在程序中这些处理后的数据将被送入业务逻辑单元,进行进一步的处理,最终向客户端返回HTTP响应。本节我们通过FastAPI实现大文件断点续传等示例详细讨论FastAPI向客户端返回HTTP响应的内容。

1、Response入门

在HTTP协议中,HTTP响应报文主要由三部分组成:

  1. 状态行 (Status Line)

    状态行包含: HTTP版本号 (如HTTP/1.1、HTTP/2。)、状态码 (- 如 200、404、500,代表服务器对请求的处理结果。)、状态描述( 如 OK、Not Found、Internal Server Error,是简单的人类可读的描述)

  2. 响应头部 (Response Headers)

    一系列 键值对,告诉客户端一些关于响应报文本身或者服务器的信息。常见的响应头字段有:

    • Content-Type: 指定返回内容的类型,比如 text/html; charset=UTF-8
    • Content-Length: 返回内容的长度(单位:字节)
    • Server: 服务器软件的信息
    • Set-Cookie: 设置客户端的 Cookie
    • Cache-Control: 缓存策略
    • Location: 重定向地址(配合 301/302 状态码)
  3. 响应主体 (Response Body)

    主体部分是服务器真正返回给客户端的数据内容。比如:

    • HTML 代码
    • 图片
    • JSON 格式的数据
    • 二进制文件流

一个常见的HTTP响应报文大致如下:

复制代码
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 137
Connection: keep-alive
Server: nginx/1.18.0

<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>

在FastAPI中可以使用Response或其派生对象方便地设置状态码、响应头以及响应主体。

2、Response基本操作

设置响应体(返回数据)

在FastAPI中,可以通过直接返回 dict / list(这时FastAPI 自动转成 JSON)、返回 HTML、文件、流式响应或用 Response 或 StreamingResponse 等手动控制来设置响应体。

python 复制代码
from fastapi import FastAPI

app = FastAPI()

@app.get("/json")
def read_json():
    return {"message": "Hello, World"}

设置状态码

FastAPI中定义了status枚举,用于在程序中指定响应状态码。

python 复制代码
from fastapi import FastAPI, status

app = FastAPI()

@app.post("/create", status_code=status.HTTP_201_CREATED)
def create_item():
    return {"msg": "Item created"}

设置响应头

可以用 Response 或 JSONResponse 手动设置头部,也可以在路由里加 response_model、response_class等参数。

python 复制代码
from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()

@app.get("/custom-header")
def custom_header():
    content = {"message": "Custom header"}
    headers = {"X-Custom-Header": "FastAPI"}
    return JSONResponse(content=content, headers=headers)

设置 Cookies

使用 Response.set_cookie() 方法

python 复制代码
from fastapi import FastAPI, Response

app = FastAPI()

@app.get("/set-cookie")
def set_cookie(response: Response):
    response.set_cookie(key="session_id", value="abc123")
    return {"message": "Cookie set"}

3、响应模型 response_model

在FastAPI中,response_model主要用来声明和验证返回数据格式。FastAPI 会根据定义的 response_model,完成:

  • 自动校验返回的数据是否符合
  • 自动生成 OpenAPI 文档(自动文档超好看)
  • 自动进行数据的序列化(比如只返回你指定的字段)
python 复制代码
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserOut(BaseModel):
    id: int
    name: str

@app.get("/user", response_model=UserOut)
def get_user():
    # 返回一个字典,FastAPI会根据UserOut去校验和生成响应
    return {"id": 1, "name": "Alice", "password": "123456"}

说明

  • password字段不会返回给前端!因为 UserOut 没有定义 password 字段。

4、响应类型 response_class

在FastAPI中, response_class主要用来指定响应体的类型和格式。FastAPI 会根据response_class控制返回的内容格式,比如 JSON、HTML、纯文本、文件、流式响应等等。

Response派生类

FastAPI提供了JSONResponse、HTMLResponse、PlainTextResponse、FileResponse、StreamingResponse、RedirectResponse等response_class用于定义响应体的类型和格式,它们均派生自 Response 类。
Response +content +status_code +headers +media_type +background JSONResponse +media_type = "application/json" +render() HTMLResponse +media_type = "text/html" +render() PlainTextResponse +media_type = "text/plain" +render() FileResponse +return files StreamingResponse +streaming data UJSONResponse +faster JSON serialization

一般情况下,FastAPI默认使用JSONResponse,返回application/json 响应。下面示例中指定了response_class为HTMLResponse以返回 HTML 字符串。

python 复制代码
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get("/html", response_class=HTMLResponse)
def get_html():
    return "<h1>Hello, HTML</h1>"

自定义response_class

我们可以通过以下几步自定义一个 Response 类:

  1. 继承自 fastapi.Response 或 starlette.responses.Response
  2. 重写 render() 方法,告诉 FastAPI:怎么把内容转成字节流(bytes)
  3. 还可以自定义 media_type(Content-Type)

自定义一个返回 .csv 文件的 Response

python 复制代码
from fastapi import FastAPI
from starlette.responses import Response

class CSVResponse(Response):
    media_type = "text/csv"

    def render(self, content: str) -> bytes:
        # 直接把字符串编码成bytes返回
        return content.encode("utf-8")

app = FastAPI()

@app.get("/csv", response_class=CSVResponse)
def get_csv():
    csv_content = "id,name\n1,Alice\n2,Bob"
    return csv_content

自定义一个加密后的 JSON Response

python 复制代码
import json
from fastapi import FastAPI
from starlette.responses import Response
import base64

class EncryptedJSONResponse(Response):
    media_type = "application/json"

    def render(self, content: dict) -> bytes:
        json_data = json.dumps(content)
        # 简单加密:base64编码(真实项目要用AES/自定义加密)
        encrypted = base64.b64encode(json_data.encode("utf-8"))
        return encrypted

app = FastAPI()

@app.get("/encrypted", response_class=EncryptedJSONResponse)
def get_encrypted():
    return {"msg": "secret data"}

自定义一个超大文件分块下载的 Response

python 复制代码
from starlette.responses import StreamingResponse

class LargeFileResponse(StreamingResponse):
    def __init__(self, file_path: str, chunk_size: int = 1024 * 1024):
        generator = self.file_chunk_generator(file_path, chunk_size)
        super().__init__(generator, media_type="application/octet-stream")
        self.headers["Content-Disposition"] = f"attachment; filename={file_path.split('/')[-1]}"

    @staticmethod
    def file_chunk_generator(file_path: str, chunk_size: int):
        with open(file_path, mode="rb") as file:
            while chunk := file.read(chunk_size):
                yield chunk

@app.get("/download")
def download_big_file():
    return LargeFileResponse("bigfile.zip")

实现断点续传(支持 Range )的 StreamingResponse

HTTP协议有个标准:Range 请求头。客户端可以在请求头里加Range: bytes=1000-,意思是:"我只要从第1000个字节开始的数据,后面的。"

这样可以实现大文件断点续传或音频、视频流式播放(在线播放时,只请求一部分)。

python 复制代码
from fastapi import FastAPI, Request, HTTPException
from starlette.responses import StreamingResponse
import os

app = FastAPI()

# 分块读取文件
def range_file_reader(file_path: str, start: int = 0, end: int = None, chunk_size: int = 1024 * 1024):
    with open(file_path, "rb") as f:
        f.seek(start)
        remaining = (end - start + 1) if end else None

        while True:
            read_size = chunk_size if not remaining else min(remaining, chunk_size)
            data = f.read(read_size)
            if not data:
                break
            yield data
            if remaining:
                remaining -= len(data)
                if remaining <= 0:
                    break

@app.get("/download")
async def download_file(request: Request):
    file_path = "bigfile.zip"  # 换成你的大文件路径

    if not os.path.exists(file_path):
        raise HTTPException(status_code=404, detail="File not found")

    file_size = os.path.getsize(file_path)
    range_header = request.headers.get("range")
    
    if range_header:
        # 解析 range头
        bytes_unit, byte_range = range_header.split("=")
        start_str, end_str = byte_range.split("-")
        start = int(start_str) if start_str else 0
        end = int(end_str) if end_str else file_size - 1

        if start >= file_size:
            raise HTTPException(status_code=416, detail="Range Not Satisfiable")

        content_length = end - start + 1
        headers = {
            "Content-Range": f"bytes {start}-{end}/{file_size}",
            "Accept-Ranges": "bytes",
            "Content-Length": str(content_length),
            "Content-Type": "application/octet-stream",
            "Content-Disposition": f"attachment; filename={os.path.basename(file_path)}",
        }

        return StreamingResponse(
            range_file_reader(file_path, start, end),
            status_code=206,  # Partial Content
            headers=headers,
        )
    
    # 没有Range头,普通全量返回
    headers = {
        "Content-Length": str(file_size),
        "Content-Type": "application/octet-stream",
        "Content-Disposition": f"attachment; filename={os.path.basename(file_path)}",
    }

    return StreamingResponse(
        range_file_reader(file_path),
        status_code=200,
        headers=headers,
    )

在此基础上,还可以:

  • 支持多段 Range(比如同时请求0-100, 200-300),但是这个场景很少,比较复杂
  • 限制最大单次传输大小(保护服务器)
  • 支持 gzip 压缩返回(如果是文本文件)
  • 加上异步读取(aiofiles)提升 IO 性能

总之,通过自定义response_class可以实现非常多且实用的功能。

相关推荐
老胖闲聊10 分钟前
Python PyAutoGUI库【GUI 自动化库】深度解析与实战指南
python
程序员JerrySUN1 小时前
驱动开发硬核特训 · Day 22(下篇): # 深入理解 Power-domain 框架:概念、功能与完整代码剖析
linux·开发语言·驱动开发·嵌入式硬件
游离状态的猫12 小时前
JavaScript性能优化实战:从瓶颈定位到极致提速
开发语言·javascript·性能优化
小彭努力中2 小时前
7.Three.js 中 CubeCamera详解与实战示例
开发语言·前端·javascript·vue.js·ecmascript
fen_fen2 小时前
Python3:Jupyter Notebook 安装和配置
ide·python·jupyter
一只程序烽.3 小时前
err: Error: Request failed with status code 400
java·axios·web
why1513 小时前
腾讯(QQ浏览器)后端开发
开发语言·后端·golang
charade3123 小时前
【C语言】内存分配的理解
c语言·开发语言·c++
LinDaiuuj3 小时前
判断符号??,?. ,! ,!! ,|| ,&&,?: 意思以及举例
开发语言·前端·javascript