Skip to content

设计响应 ​

同一个文章服务有时返回页面,有时返回 JSON,也可能让用户下载文件。响应体、状态码和响应头共同决定客户端如何处理结果。本专题从最常见的文本、JSON、HTML 开始,再解决创建、跳转和下载等问题。

Starlette 的响应类会替我们构造 ASGI 消息。处理函数只需返回合适的响应对象;需要表达创建成功或重定向时,再设置状态码与响应头。

启动 04_article_responses.py 后,可运行 response_demo.py 查看状态码与响应体。 02_status_redirect_cookie.py 的创建响应另有独立演示函数。

先比较三个重点:

  • 普通说明用 PlainTextResponse
  • 结构化接口数据用 JSONResponse
  • 少量页面内容用 HTMLResponse

它们都属于响应对象,但媒体类型不同。

项目结构仍然保持简单:

text
p01c03/
└── 01_basic_responses.py

示例文件:01_basic_responses.py。

运行:uvicorn 01_basic_responses:app --reload。

py
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)

启动:

bash
uvicorn 01_basic_responses:app --reload

然后访问:

text
http://127.0.0.1:8000/hello-text
http://127.0.0.1:8000/article-json
http://127.0.0.1:8000/about-html

第一个接口:

python
return PlainTextResponse("Hello, Starlette Response")

表示返回普通文本。

第二个接口:

python
return JSONResponse(article)

表示把 Python 字典作为 JSON 返回。

第三个接口:

python
return HTMLResponse(html)

表示返回一段 HTML 字符串,让浏览器按页面方式渲染。

重点不是背类名,而是理解:

text
endpoint 不只是处理请求
还要决定客户端应该怎样理解返回内容

可以先记:

text
PlainTextResponse → 普通文本
JSONResponse      → JSON 数据
HTMLResponse      → HTML 页面内容

三种基础响应对象的处理与媒体类型协商

创建资源通常返回 201;跳转用重定向响应。响应头传递附加信息,Cookie 则通过 Set-Cookie 响应头交给客户端保存。

前面的例子只返回了内容。现在继续看响应里的其他部分:

text
状态码
响应头
Cookie
重定向

示例文件:02_status_redirect_cookie.py。

运行:uvicorn 02_status_redirect_cookie:app --reload。

py
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)

这段代码分别演示了四件事。

第一,设置状态码:

python
JSONResponse(..., status_code=201)

表示这次创建成功,状态码用 201 而不是默认的 200。

第二,设置响应头:

python
response.headers["X-App-Name"] = "Starlette Course"

第三,设置 Cookie:

python
response.set_cookie("visit_token", "p01c03-demo")

第四,重定向:

python
RedirectResponse("/articles")

表示让客户端跳转到 /articles。

这里最重要的是明白:

text
响应对象不只有“内容”
还可以控制状态码、响应头、Cookie 和跳转行为

状态码控制、Cookie 下发与 HTTP 重定向机制

文件下载与流式响应 ​

文件响应适合已有文件;流式响应按迭代结果逐步发送内容。两者解决的问题不同,需根据数据来源选择。

有些场景不是返回字符串或 JSON,而是返回文件,或者边生成边返回内容。

示例文件:03_file_and_stream.py。运行:uvicorn 03_file_and_stream:app --reload。

py
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)

同时在当前目录准备一个文件:

text
course_intro.txt

文件内容可以随便写几行课程介绍。

FileResponse:

python
FileResponse(README_FILE, filename="course_intro.txt")

用于把本地文件直接返回给客户端。

StreamingResponse:

python
StreamingResponse(stream_text(), media_type="text/plain")

用于把一个可迭代生成的内容逐段返回出去。

这部分重点只要记住:

text
FileResponse 适合返回文件
StreamingResponse 适合分段返回内容

FileResponse 与 StreamingResponse 传输机制对比

文章服务的响应设计 ​

文章服务会按访问目的返回不同类型的内容。

目标包括:

text
/               返回文本首页
/articles       返回 JSON 列表
/about          返回 HTML 说明
/old-articles   重定向到 /articles
/download       返回文件
/stream         返回流式文本

示例文件:04_article_responses.py。运行:uvicorn 04_article_responses:app --reload。

py
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)

案例响应速览 ​

这个案例在同一个应用里组合多种响应方式:

text
文本响应
JSON 响应
HTML 响应
重定向响应
文件响应
流式响应

它的重点不是业务复杂,而是帮助你建立一个判断:

text
不同场景
选择不同响应对象

响应对象选择 ​

对应关系是:

text
返回一句话            → PlainTextResponse
返回接口数据          → JSONResponse
返回简单页面内容      → HTMLResponse
跳转到另一个地址      → RedirectResponse
返回本地文件          → FileResponse
边生成边返回          → StreamingResponse

Starlette 响应对象设计与选型决策矩阵

案例源码 ​

py
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()