在“FastAPI系列05:FastAPI请求(Request)”一节中我们详细了解了FastAPI程序对请求参数的校验和处理,在程序中这些处理后的数据将被送入业务逻辑单元,进行进一步的处理,最终向客户端返回HTTP响应。本节我们通过FastAPI实现大文件断点续传等示例详细讨论FastAPI向客户端返回HTTP响应的内容。
在HTTP协议中,HTTP响应报文主要由三部分组成:
状态行 (Status Line)
状态行包含: HTTP版本号(如HTTP/1.1、HTTP/2。)、状态码(- 如 200、404、500,代表服务器对请求的处理结果。)、状态描述( 如 OK、Not Found、Internal Server Error,是简单的人类可读的描述)
响应头部 (Response Headers)
一系列 键值对,告诉客户端一些关于响应报文本身或者服务器的信息。常见的响应头字段有:
响应主体 (Response Body)
主体部分是服务器真正返回给客户端的数据内容。比如:
一个常见的HTTP响应报文大致如下:
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 137
Connection: keep-alive
Server: nginx/1.18.0
Hello
Hello, World!
在FastAPI中可以使用Response
或其派生对象方便地设置状态码、响应头以及响应主体。
在FastAPI中,可以通过直接返回 dict / list(这时FastAPI 自动转成 JSON)、返回 HTML、文件、流式响应或用 Response 或 StreamingResponse 等手动控制来设置响应体。
from fastapi import FastAPI
app = FastAPI()
@app.get("/json")
def read_json():
return {"message": "Hello, World"}
FastAPI中定义了status
枚举,用于在程序中指定响应状态码。
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等参数。
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)
使用 Response.set_cookie() 方法
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"}
在FastAPI中,response_model主要用来声明和验证返回数据格式。FastAPI 会根据定义的 response_model,完成:
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"}
说明:
在FastAPI中, response_class主要用来指定响应体的类型和格式。FastAPI 会根据response_class控制返回的内容格式,比如 JSON、HTML、纯文本、文件、流式响应等等。
FastAPI提供了JSONResponse、HTMLResponse、PlainTextResponse、FileResponse、StreamingResponse、RedirectResponse等response_class用于定义响应体的类型和格式,它们均派生自 Response 类。
一般情况下,FastAPI默认使用JSONResponse,返回application/json 响应。下面示例中指定了response_class为HTMLResponse以返回 HTML 字符串。
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/html", response_class=HTMLResponse)
def get_html():
return "Hello, HTML
"
我们可以通过以下几步自定义一个 Response 类:
自定义一个返回 .csv 文件的 Response
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
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
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个字节开始的数据,后面的。”
这样可以实现大文件断点续传或音频、视频流式播放(在线播放时,只请求一部分)。
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,
)
在此基础上,还可以:
总之,通过自定义response_class可以实现非常多且实用的功能。