设计响应
同一个文章服务有时返回页面,有时返回 JSON,也可能让用户下载文件。响应体、状态码和响应头共同决定客户端如何处理结果。本专题从最常见的文本、JSON、HTML 开始,再解决创建、跳转和下载等问题。
Starlette 的响应类会替我们构造 ASGI 消息。处理函数只需返回合适的响应对象;需要表达创建成功或重定向时,再设置状态码与响应头。
启动 04_article_responses.py 后,可运行 response_demo.py 查看状态码与响应体。 02_status_redirect_cookie.py 的创建响应另有独立演示函数。
先比较三个重点:
- 普通说明用
PlainTextResponse - 结构化接口数据用
JSONResponse - 少量页面内容用
HTMLResponse
它们都属于响应对象,但媒体类型不同。
项目结构仍然保持简单:
p01c03/
└── 01_basic_responses.py示例文件:01_basic_responses.py。
运行:uvicorn 01_basic_responses:app --reload。
from starlette.applications import Starlette
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
from starlette.routing import Route
async def hello_text(request):
return PlainTextResponse("Hello, Starlette Response")
async def article_json(request):
article = {
"id": 1,
"title": "Starlette Response 入门",
"author": "Python 讲师",
}
return JSONResponse(article)
async def about_html(request):
html = """
<html>
<head>
<title>Starlette 小课</title>
</head>
<body>
<h1>Starlette 小课</h1>
<p>这一章我们正在学习 Response 响应对象。</p>
</body>
</html>
"""
return HTMLResponse(html)
routes = [
Route("/hello-text", hello_text),
Route("/article-json", article_json),
Route("/about-html", about_html),
]
app = Starlette(routes=routes)启动:
uvicorn 01_basic_responses:app --reload然后访问:
http://127.0.0.1:8000/hello-text
http://127.0.0.1:8000/article-json
http://127.0.0.1:8000/about-html第一个接口:
return PlainTextResponse("Hello, Starlette Response")表示返回普通文本。
第二个接口:
return JSONResponse(article)表示把 Python 字典作为 JSON 返回。
第三个接口:
return HTMLResponse(html)表示返回一段 HTML 字符串,让浏览器按页面方式渲染。
重点不是背类名,而是理解:
endpoint 不只是处理请求
还要决定客户端应该怎样理解返回内容可以先记:
PlainTextResponse → 普通文本
JSONResponse → JSON 数据
HTMLResponse → HTML 页面内容状态码、跳转与 Cookie
创建资源通常返回 201;跳转用重定向响应。响应头传递附加信息,Cookie 则通过 Set-Cookie 响应头交给客户端保存。
前面的例子只返回了内容。现在继续看响应里的其他部分:
状态码
响应头
Cookie
重定向示例文件:02_status_redirect_cookie.py。
运行:uvicorn 02_status_redirect_cookie:app --reload。
from starlette.applications import Starlette
from starlette.responses import JSONResponse, RedirectResponse
from starlette.routing import Route
async def create_article(request):
response = JSONResponse(
{
"id": 100,
"title": "新文章标题",
"message": "文章创建成功",
},
status_code=201,
headers={"X-App-Name": "Starlette Course"},
)
response.set_cookie("visit_token", "p01c03-demo")
return response
async def old_articles(request):
return RedirectResponse("/articles")
async def article_list(request):
return JSONResponse(
{
"items": [
{"id": 1, "title": "认识 Starlette 应用对象"},
{"id": 2, "title": "学习 Request 请求对象"},
]
}
)
routes = [
Route("/create-article", create_article, methods=["POST"]),
Route("/old-articles", old_articles),
Route("/articles", article_list),
]
app = Starlette(routes=routes)
if __name__ == '__main__':
import uvicorn
uvicorn.run("02_status_redirect_cookie:app", host='127.0.0.1', port=8000, reload=True)这段代码分别演示了四件事。
第一,设置状态码:
JSONResponse(..., status_code=201)表示这次创建成功,状态码用 201 而不是默认的 200。
第二,设置响应头:
response.headers["X-App-Name"] = "Starlette Course"第三,设置 Cookie:
response.set_cookie("visit_token", "p01c03-demo")第四,重定向:
RedirectResponse("/articles")表示让客户端跳转到 /articles。
这里最重要的是明白:
响应对象不只有“内容”
还可以控制状态码、响应头、Cookie 和跳转行为文件下载与流式响应
文件响应适合已有文件;流式响应按迭代结果逐步发送内容。两者解决的问题不同,需根据数据来源选择。
有些场景不是返回字符串或 JSON,而是返回文件,或者边生成边返回内容。
示例文件:03_file_and_stream.py。运行:uvicorn 03_file_and_stream:app --reload。
from pathlib import Path
from starlette.applications import Starlette
from starlette.responses import FileResponse, StreamingResponse
from starlette.routing import Route
BASE_DIR = Path(__file__).resolve().parent
async def download_course(request):
file_path = BASE_DIR / "course.txt"
return FileResponse(file_path, filename="course.txt")
def generate_lines():
yield "第 1 行:正在生成内容\n"
yield "第 2 行:继续生成内容\n"
yield "第 3 行:内容生成完成\n"
async def stream_text(request):
return StreamingResponse(generate_lines(), media_type="text/plain")
routes = [
Route("/download", download_course),
Route("/stream", stream_text),
]
app = Starlette(routes=routes)
if __name__ == '__main__':
import uvicorn
uvicorn.run("03_file_and_stream:app", host='127.0.0.1', port=8000, reload=True)同时在当前目录准备一个文件:
course_intro.txt文件内容可以随便写几行课程介绍。
FileResponse:
FileResponse(README_FILE, filename="course_intro.txt")用于把本地文件直接返回给客户端。
StreamingResponse:
StreamingResponse(stream_text(), media_type="text/plain")用于把一个可迭代生成的内容逐段返回出去。
这部分重点只要记住:
FileResponse 适合返回文件
StreamingResponse 适合分段返回内容文章服务的响应设计
文章服务会按访问目的返回不同类型的内容。
目标包括:
/ 返回文本首页
/articles 返回 JSON 列表
/about 返回 HTML 说明
/old-articles 重定向到 /articles
/download 返回文件
/stream 返回流式文本示例文件:04_article_responses.py。运行:uvicorn 04_article_responses:app --reload。
from pathlib import Path
from starlette.applications import Starlette
from starlette.responses import (
FileResponse,
HTMLResponse,
JSONResponse,
PlainTextResponse,
RedirectResponse,
StreamingResponse,
)
from starlette.routing import Route
BASE_DIR = Path(__file__).resolve().parent
ARTICLES = [
{"id": 1, "title": "认识 Starlette 应用对象"},
{"id": 2, "title": "学习 Request 请求对象"},
{"id": 3, "title": "学习 Response 响应对象"},
]
async def homepage(request):
return PlainTextResponse("欢迎学习 Response 响应对象")
async def article_list(request):
return JSONResponse({"items": ARTICLES})
async def about(request):
html = """
<html>
<head>
<title>关于页面</title>
</head>
<body>
<h1>关于本课程</h1>
<p>这里演示 HTMLResponse 的使用方式。</p>
</body>
</html>
"""
return HTMLResponse(html)
async def old_articles(request):
return RedirectResponse("/articles")
async def download(request):
file_path = BASE_DIR / "course.txt"
return FileResponse(file_path, filename="course.txt")
def stream_lines():
yield "第 1 行:开始输出\n"
yield "第 2 行:继续输出\n"
yield "第 3 行:输出完成\n"
async def stream(request):
return StreamingResponse(stream_lines(), media_type="text/plain")
routes = [
Route("/", homepage),
Route("/articles", article_list),
Route("/about", about),
Route("/old-articles", old_articles),
Route("/download", download),
Route("/stream", stream),
]
app = Starlette(routes=routes)
if __name__ == '__main__':
import uvicorn
uvicorn.run("04_article_responses:app", host='127.0.0.1', port=8000, reload=True)案例响应速览
这个案例在同一个应用里组合多种响应方式:
文本响应
JSON 响应
HTML 响应
重定向响应
文件响应
流式响应它的重点不是业务复杂,而是帮助你建立一个判断:
不同场景
选择不同响应对象响应对象选择
对应关系是:
返回一句话 → PlainTextResponse
返回接口数据 → JSONResponse
返回简单页面内容 → HTMLResponse
跳转到另一个地址 → RedirectResponse
返回本地文件 → FileResponse
边生成边返回 → StreamingResponse案例源码
import requests
BASE_URL = "http://127.0.0.1:8000"
def test_response_types():
for path in ("/", "/articles", "/about"):
response = requests.get(f"{BASE_URL}{path}", timeout=5)
print(path, response.status_code, response.headers.get("content-type"))
print(response.text[:160])
def test_redirect():
response = requests.get(
f"{BASE_URL}/old-articles", allow_redirects=False, timeout=5
)
print("GET /old-articles", response.status_code)
print("Location:", response.headers.get("location"))
def test_download():
response = requests.get(f"{BASE_URL}/download", timeout=5)
print("GET /download", response.status_code)
print("Content-Disposition:", response.headers.get("content-disposition"))
print(response.text)
def test_stream():
response = requests.get(f"{BASE_URL}/stream", timeout=5)
print("GET /stream", response.status_code)
print(response.text)
def test_create_response_in_app2():
response = requests.post(f"{BASE_URL}/create-article", timeout=5)
print("POST /create-article", response.status_code)
print(response.json())
print("X-Course-Name:", response.headers.get("X-Course-Name"))
print("Set-Cookie:", response.headers.get("set-cookie"))
if __name__ == "__main__":
# 启动 04_article_responses.py 后运行前四项;启动 02_status_redirect_cookie.py 后可单独调用最后一项。
test_response_types()
test_redirect()
test_download()
test_stream()