Python 现代开发学习笔记

(十五)重复提交与错误反馈

1. 保存之后再读取页面

表单保存成功后返回 303,Location 指向 /,浏览器随后改用 GET 读取。这个“POST → Redirect → GET”的过程常简称 PRG:刷新当前 GET 页面,不会重新发送刚才的新增表单。

为了逐项观察响应,先准备独立演示的运行环境。BookApp 是这里的书架应用,HTTPX 的 WSGITransport 直接按 WSGI 接口调用它;临时数据库让各个演示从空书架开始。

# 项目文件:web_project/src/webbook/lab.py
# 独立演示每次使用临时数据库;WSGITransport直接调用应用,便于对照具体状态码。
import tempfile
from contextlib import contextmanager
from pathlib import Path

import httpx

from .app import BookApp
from .database import initialize


@contextmanager
def lab_client():
    with tempfile.TemporaryDirectory(prefix="web-book-") as folder:
        database = Path(folder) / "library.sqlite3"
        initialize(database)
        app = BookApp(
            database, origin="http://book.test", cors_origins={"http://front.test"}
        )
        transport = httpx.WSGITransport(app=app)
        with httpx.Client(
            transport=transport, base_url="http://book.test", follow_redirects=False
        ) as client:
            yield client, app, database


def login(client, username="alice", password="learn-alice"):
    token = client.get("/api/session").json()["csrf_token"]
    response = client.post(
        "/login", data={"username": username, "password": password, "csrf_token": token}
    )
    if response.status_code != 303:
        raise RuntimeError("演示登录没有完成")
    return client.get("/api/session").json()["csrf_token"]

lab_client 复用前面 contextmanager 的用法,with 结束后关闭客户端并清理临时目录。login 则按“取令牌 → POST 登录 → 取新令牌”的顺序返回登录会话的令牌,供下面各个演示使用。

# 项目文件:demos/form_redirect.py
# POST成功返回303,随后GET读取;刷新GET不会再次发送原来的新增操作。
from webbook.lab import lab_client, login

with lab_client() as (client, _app, _database):
    csrf = login(client)
    response = client.post(
        "/books", data={"title": "表单书籍", "tags": "网络,Python", "csrf_token": csrf}
    )
    print("POST", response.status_code, response.headers["location"])
    print("跳转GET", client.get(response.headers["location"]).status_code)
    client.get("/")
    print("刷新后数量", len(client.get("/api/books").json()["items"]))
    again = client.post("/books", data={"title": "表单书籍", "csrf_token": csrf})
    print("再次提交", again.status_code)

前面三行输出为 POST 303、跳转 GET 200、刷新后数量 1。用户再次主动提交相同表单时,数据库唯一约束让它返回 409。PRG 改变成功后的页面导航,唯一约束处理重复记录,两者分别解决不同步骤的问题。

表单POST插入提交后返回303,浏览器跟随Location发GET,刷新只重复读取,第二次主动POST由唯一约束判断

2. 按实际失败位置给出响应

# 项目文件:demos/error_cases.py
# 每份请求只改一个位置,观察状态码怎样区分身份、格式、字段和业务冲突。
from webbook.lab import lab_client, login

with lab_client() as (client, _app, _database):
    print("未登录", client.get("/api/books").status_code)
    token = login(client)
    headers = {"X-CSRF-Token": token}
    data = {"title": "Python Web", "finished": False, "tags": []}
    print("创建", client.post("/api/books", json=data, headers=headers).status_code)
    print("重复", client.post("/api/books", json=data, headers=headers).status_code)
    print(
        "字段类型",
        client.post(
            "/api/books", json={**data, "finished": "false"}, headers=headers
        ).status_code,
    )
    print(
        "内容类型",
        client.post("/api/books", data={"title": "A"}, headers=headers).status_code,
    )
    print(
        "JSON结构",
        client.post(
            "/api/books",
            content="{",
            headers={**headers, "Content-Type": "application/json"},
        ).status_code,
    )
    print("缺令牌", client.post("/api/books", json=data).status_code)
    print("未找到", client.get("/api/books/999").status_code)
    response = client.put("/api/books")
    print("方法", response.status_code, response.headers["allow"])
状态 本章的实际触发 数据库中的结果
400 JSON 文本缺少结构、UTF-8 解码失败 不新增
401 没有登录身份,或密码不正确 不取得对应权限
403 令牌不符、来源未许可或书籍归属不符 不执行该写入
404 地址或书籍编号没有找到 不新增
405 路径不支持该请求方法 不执行该动作
409 同账号已保存同名书籍 保留已有一条
413 书籍请求体超过任务规定大小 不读取为书籍
415 入口需要 JSON,却提交表单类型 不进入字段校验
422 数据已经解析,但字段或查询不符合规则 显示需要修改的字段
500 请求中的数据库操作失败 回滚本次事务

客户端断开时,也可能已经完成了数据库提交、只是没有收到响应。重新查看列表能核对当前记录;不能仅凭“页面没有显示成功”就认定数据库一定没写入。

请求从格式身份令牌字段归属到数据库逐层推进,每种失败停止在对应位置,数据库失败回滚已有插入

笔记正文与全部题解 · Ctrl / ⌘ K