码上问学问代码之理,学编程之道

第 10 章 · 中间件、请求钩子与消息闪现

「每个请求都要记一条日志」「每个响应都要加跨域头」「每次请求都要先看有没有登录」——这些事如果写在每个视图里,就是无休止的复制粘贴。本章教你用请求钩子把公共逻辑插到请求生命周期的固定点上,一次写好、全局生效;最后再补上一个服务端渲染时代的经典知识点——flash 消息闪现。

请求钩子是什么

在请求生命周期的固定点插队
一次请求的钩子时间线 请求进来 → 出去 before_request 进门先过一道关 视图函数 真正处理业务 after_request 出门再加工一下 teardown_request(收尾) 无论成功失败都执行:关连接、写日志、释放资源 钩子就是在生命周期的「固定点」插队:进门、出门、收尾各就各位

钩子让代码在请求生命周期的固定节点自动运行:请求进门先过 before_request(鉴权/日志/预置),出门前经 after_request(加头/算耗时),最后 teardown_request 收尾(无论成败都执行)。

先讲为什么一栋大楼里,每个人进出都要刷卡、都要在登记本上签一笔。如果你把这套流程分给每个房间自己管,效果一定乱七八糟。公共流程应该设在大门口,一次管住所有人。
Web 请求也是一样:「记日志」「加跨域头」「检查登录状态」这类事不应该由视图函数负责。请求钩子(request hooks)就是在请求生命周期的固定节点上挂一个函数,所有请求都会经过它——这既是代码复用,也是一致性的保证。
讲透:请求的一生会经过哪些「站点」
  • before_request:请求刚进来、还没找到视图函数之前。适合记日志、统一鉴权、给 g 预置数据。
  • 视图函数:真正干活的函数。
  • after_request:视图返回了响应对象之后、响应发出去之前。适合加响应头、算耗时、统一包装。
  • teardown_request:请求彻底结束(包括出错)时。适合关闭资源、清理临时数据。
  • Flask 与 Django 的差别:Flask 并没有 Django 那种「一层套一层」的 middleware 概念,它用钩子实现同样的效果。真要在 WSGI 层面插手(比如改写 REMOTE_ADDR),才需要 app.wsgi_app = 中间件(app.wsgi_app)。
  • 钩子按「作用范围」分两种:@app.before_request 对全应用生效;写在蓝图里的 @bp.before_request 只对该蓝图的请求生效,@bp.before_app_request 则对全应用生效。

before_request:请求进门先过一道关

日志 / 鉴权 / 预置 g
动手做@app.before_request 装饰的函数,在每次请求的视图执行之前都会被调用。它不需要参数(要用信息就从 request 里取),返回 None 表示「放行」,返回一个响应对象就表示「到此为止,直接返回这个响应」。下面写三个最常见的用法。
apps/__init__.pypython
import time
from flask import Flask, g, request, jsonify

app = Flask(__name__)


@app.before_request
def before_every_request():
    # ① 给每个请求打上起始时间戳,配合 after_request 算耗时
    g.start_time = time.perf_counter()

    # ② 记录一条请求日志(真实项目会写进日志文件)
    app.logger.info(">> %s %s", request.method, request.path)

    # ③ 统一给 g 预置一个默认用户,视图里可以放心判断 g.user 是否存在
    g.user = None

    # ④ 返回 None = 放行,继续走视图函数
    return None
运行输出Python 运行结果
运行结果: $ curl http://127.0.0.1:5000/api/article/list # 终端(flask 日志)输出: >> GET /api/article/list 127.0.0.1 - - [12/Oct/2026 10:30:00] "GET /api/article/list HTTP/1.1" 200 - # 说明:before_request 在每个请求前都执行一次, # 视图函数里可以直接用 g.start_time,无需自己记时间
讲透
  • 「返回非 None 就短路」是 before_request 的关键特性:这正好能实现「统一鉴权」——在钩子里判断 token 是否合法,不合法就 return jsonify(code=401, ...),视图函数压根不会执行。
  • 但绝大多数接口「不需要登录」,钩子无法直接分辨。所以做统一鉴权时要配一张「白名单」(如 /api/user/login、/api/article/list),或者干脆仍用第 8 章的 @login_required 装饰器——哪个更合适取决于你的接口画像。
  • g 与钩子是天作之合:钩子里写进 g,视图里直接读,全程不用传参。这正是第 4 章 g 对象设计出来要解决的问题。
  • 用 app.logger 而不是 print:Flask 的 logger 会带时间戳、级别,也能配置输出到文件,比 print 规范得多。
避坑写 before_request 时,函数末尾一定要么什么都不返回、要么明确 return None。新手常犯的错是顺手写一句 return "ok" 做「调试」,结果所有接口都返回了字符串 "ok",视图全部不执行。在钩子里除非你确实想拦住请求,否则永远不要 return 响应对象。

after_request:响应出门前再动一下

加响应头 / 算耗时
动手做@app.after_request 的函数必须接收一个参数(响应对象)并返回它。视图已经干完活了,你在这里只能「改」响应,不能改「要不要返回」。
apps/__init__.py(续)python
@app.after_request
def after_every_request(response):
    # ① 计算本次请求耗时,写进自定义响应头(方便前端与运维排查)
    if hasattr(g, "start_time"):
        cost_ms = (time.perf_counter() - g.start_time) * 1000
        response.headers["X-Response-Time"] = f"{cost_ms:.2f}ms"

    # ② 统一加安全相关的响应头
    response.headers["X-Content-Type-Options"] = "nosniff"

    # ③ 必须把 response 返回出去,否则请求会报错
    return response
运行输出Python 运行结果
运行结果: $ curl -i http://127.0.0.1:5000/api/article/list HTTP/1.1 200 OK Content-Type: application/json X-Response-Time: 3.47ms X-Content-Type-Options: nosniff {"code": 200, "msg": "success", "data": { "list": [], "total": 6, "page": 1, "page_size": 10 }} # 每一条响应都自动带上了这两个头,视图函数一行都没写
讲透
  • 忘了 return response 是头号事故:返回 None 会让 Flask 报 TypeError: The view function did not return a valid response 之类的错,而且报错位置看起来在视图上,极难排查。
  • after_request 一定会执行吗:只要请求被视图或 before_request 正常处理出响应,它就会执行。但如果请求在 before_request 里就被短路返回,它依然会执行(因为已经有响应对象了)——这点常被误解。
  • 耗时统计的精度:用 time.perf_counter() 而不是 time.time(),前者是单调时钟,不受系统时间调整影响,测间隔更准。
  • 为什么用自定义响应头而不是改响应体:加头不影响接口契约,前端拿到的 JSON 结构完全不变——这是最「无侵入」的横切关注点处理方式。

teardown_request 与已废弃的 before_first_request

收尾与初始化
动手做@app.teardown_request 在请求结束时执行,它会接收一个参数:本次请求过程中抛出的异常(没出错就是 None)。它的职责是「打扫战场」——关闭文件、回滚没提交的事务、记录错误。
apps/__init__.py(续)python
from flask import Flask, g
from .models import db

app = Flask(__name__)


@app.teardown_request
def teardown(exception):
    # ① 请求出错时把没提交的事务回滚,避免脏数据残留在连接里
    if exception is not None:
        db.session.rollback()
        app.logger.error("请求异常:%s", exception)

    # ② 关闭请求期间打开的临时资源(示意)
    tmp = g.pop("tmp_file", None)
    if tmp:
        tmp.close()

    # 注意:此时响应已经准备完毕,改不了状态码和响应体了
    return None
运行输出Python 运行结果
运行结果: # 正常请求:teardown 执行,exception 为 None,无输出 # 视图里抛异常时: [ERROR] 请求异常:division by zero # 同时数据库事务被回滚,不会把半截数据留在库里
讲透
  • teardown_request 一定会执行:无论请求成功还是抛异常,它都会跑一次,所以它是「清理」这类动作的正确位置。
  • 还有 teardown_appcontext:它在应用上下文结束时执行,范围比请求更大(脚本里手动 with app.app_context() 也会触发)。Flask-SQLAlchemy 内部就是用类似机制在请求结束自动移除会话的,所以大多数时候你不需要自己写。
  • before_first_request 已经被移除:它在 Flask 2.2 被标记废弃、2.3 起彻底删除。也就是说在 Flask 3.x 里写 @app.before_first_request 会直接报 AttributeError。网上大量老教程还停留在它身上,抄之前先看版本。
  • 那「只在启动时执行一次」该放哪:放在应用创建阶段(create_app() 里)直接执行,或用扩展提供的初始化钩子。启动初始化本来就不该挂在「某个请求」上。
避坑不要拿 teardown_request 当「请求完成后的业务逻辑」用。它执行时响应已经生成,你无法再改变返回给用户的内容,而且它出错也不会反馈到用户那里。想在响应发出去之前动手,用 after_request;想清理资源,才用 teardown_request。

用钩子写一个请求日志中间件

方法 + 路径 + 耗时 + 状态码
用钩子写一个请求日志中间件 before_request g.start = time.time() 视图函数 真正处理业务 after_request 耗时 = now - g.start 输出一行日志:方法 · 路径 · 状态码 · 耗时 GET /api/article/list 200 12ms  POST /api/user/login 401 45ms 这样不用在每个接口里手写计时代码——钩子统一在进门/出门各做一次即可

用 before_request 记开始时间、after_request 算耗时,就能给所有接口统一打日志(方法、路径、状态码、耗时),不必在每个视图里重复写计时代码。

先讲为什么线上出问题时,你第一个想知道的是「哪个接口慢了、哪个接口在报错」。这两个信息在请求开始时(方法、路径)和请求结束时(状态码、耗时)各有一半,所以必须用两个钩子接力完成:before_request 记开始时间,after_request 算差值并打印。这就是一个最小可用的「请求日志中间件」。
apps/middleware.pypython
import time
from flask import g, request, current_app


def register_middleware(app):
    """把请求日志钩子注册到应用上,在 create_app() 里调用一次即可。"""

    @app.before_request
    def _log_start():
        g.start_time = time.perf_counter()

    @app.after_request
    def _log_finish(response):
        cost_ms = (time.perf_counter() - g.get("start_time", time.perf_counter())) * 1000
        current_app.logger.info(
            "%s %s -> %s  %.2fms",
            request.method,          # 请求方法
            request.full_path,       # 路径(含查询参数)
            response.status_code,    # 状态码
            cost_ms,                 # 耗时
        )
        return response
运行输出Python 运行结果
运行结果: $ flask --app apps run * Running on http://127.0.0.1:5000 # 依次访问几个接口后,终端日志: GET /api/article/list?page=1 -> 200 12.35ms POST /api/user/login -> 200 88.14ms GET /api/user/profile -> 401 1.02ms GET /api/comment/list?article_id=1 -> 200 9.76ms # 一眼看出:登录因为要做哈希校验所以最慢; # 401 那行说明有人没带 token 就访问了个人信息接口
讲透
  • 为什么用 g.get("start_time", ...):万一 before_request 因为某种原因没执行(比如被其它钩子提前中断),直接访问 g.start_time 会抛 AttributeError。给一个兜底值让日志更健壮。
  • request.full_path 比 request.path 多了查询串:排查「哪个筛选条件导致接口变慢」时非常有用。
  • 把中间件写成 register_middleware(app) 的形式,是配合第 5 章「应用工厂」的写法——create_app() 里调用一次,蓝图和应用都干净。
  • 生产环境别用 info 打全量日志:高并发下日志量惊人。常见做法是只记慢请求(超过 500ms)和错误请求(4xx / 5xx),其余降级为 debug。

用钩子统一处理 CORS 与响应包装

一次配置,全站生效
用钩子统一处理:一次配置,全站生效 after_request 里统一做两件事 加 CORS 头 + 包装响应信封 /api/user/login 自动带上 /api/article/list 自动带上 /api/comment/add 自动带上 好处:新增接口时不用再记着「加 CORS 头 / 包信封」——钩子已经替你做了 这也是「横切关注点」的典型处理位置:日志、鉴权、CORS、包装都适合放钩子

CORS 头、响应包装这类「每个接口都要做的事」适合放在 after_request 钩子里统一处理。新增接口时无需重复配置,全站自动生效。

先讲为什么Vue 前端跑在 http://localhost:5173,Flask 后端在 http://127.0.0.1:5000——域名(端口)不同,浏览器就判定为「跨域」,会拦下响应,除非后端明确回一句「我允许它访问」。这句声明就是 CORS 响应头。
而它必须出现在每一个响应上,正是 after_request 的用武之地。
动手做先用钩子手写一遍,看清 CORS 到底加了什么头;再说生产环境该怎么做。
apps/middleware.py(续)python
from flask import request

# 允许跨域访问的前端来源(精确写,不要用 *)
ALLOW_ORIGIN = "http://localhost:5173"


def register_middleware(app):
    # ① 浏览器在发「非简单请求」前会先发一个 OPTIONS 预检请求,
    #    直接放行并返回 204,不必进视图
    @app.before_request
    def _handle_preflight():
        if request.method == "OPTIONS":
            return "", 204
        return None

    # ② 在每一个响应上补上 CORS 头
    @app.after_request
    def _add_cors_headers(response):
        response.headers["Access-Control-Allow-Origin"] = ALLOW_ORIGIN
        response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
        response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
        response.headers["Access-Control-Max-Age"] = "3600"   # 预检结果缓存 1 小时
        return response
运行输出Python 运行结果
运行结果: $ curl -i -X OPTIONS http://127.0.0.1:5000/api/user/profile \ -H "Origin: http://localhost:5173" \ -H "Access-Control-Request-Method: GET" HTTP/1.1 204 NO CONTENT Access-Control-Allow-Origin: http://localhost:5173 Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 3600 # 预检通过后,浏览器才会真正发出 GET 请求
动手做同一种思路还能做「统一响应包装」:在 after_request 里检查响应是不是标准的 {code, msg, data} 结构,不是就自动包一层。这样视图里可以放心地 return jsonify(data) 而不用每次手写三个字段。
apps/middleware.py(统一响应包装)python
import json


def register_wrapper(app):
    @app.after_request
    def _wrap_json(response):
        # 只处理 JSON 响应,其它(图片、文件下载)原样放行
        if not response.is_json:
            return response

        data = response.get_json()
        # 已经是 {code, msg, data} 就不要再包一层
        if isinstance(data, dict) and "code" in data and "msg" in data:
            return response

        response.set_data(json.dumps(
            {"code": 200, "msg": "success", "data": data},
            ensure_ascii=False,
        ))
        response.content_type = "application/json; charset=utf-8"
        return response
运行输出Python 运行结果
运行结果: # 视图里写:return jsonify(article_list) # 直接返回数据本身 $ curl http://127.0.0.1:5000/api/article/list { "code": 200, "msg": "success", "data": [ { "id": 1, "title": "静夜思", "author": "李白" } ] } # 视图省掉了手写 code/msg 的样板代码,格式仍与接口文档完全一致
讲透
  • 生产环境直接用 flask-cors:CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}}) 一行搞定,它还处理了各种边界情况。手写钩子是为了让你明白它在干什么,不是为了替代它。
  • 统一响应包装的取舍:它确实省代码,但「魔法」藏在钩子里——别人读视图时不知道最后返回的结构变了。小项目推荐显式地在视图里写 success() / error() 辅助函数,比全局钩子更易读。
  • 一定要「放行非 JSON 响应」:第 9 章的文件下载、图片预览返回的是二进制,一旦被包装成 JSON 就会彻底坏掉。if not response.is_json: return response 这行是必须的。
  • 为什么 ensure_ascii=False:不加这个参数,中文会被转成 \u9759\u591c\u601d 这样的转义序列。虽然 JSON 解析后内容一样,但调试时看不清,日志也没法读。
避坑Access-Control-Allow-Origin: * 看着方便,但它和「携带 Cookie 凭证」的请求互斥——浏览器会直接拒绝。而且 * 等于允许任何网站调用你的接口。正确做法是精确列出前端地址(如 http://localhost:5173、https://你的域名)。另外别忘了把自定义的 Authorization 头写进 Access-Control-Allow-Headers,否则带了 token 的跨域请求照样被拦。

flash 消息闪现

服务端渲染的「提示条」
先讲为什么在「模板渲染」的传统做法里,表单提交成功后要 redirect 回到另一个页面——可这样一来,「操作成功」这句提示就丢了,因为重定向是一个全新的请求。flash 就是 Flask 提供的「一次性消息」机制:把消息存进 session,下一次渲染页面时取出来显示,显示完就删除。它解决的正是「重定向之后没法传提示」这个经典问题(业内称为 Post/Redirect/Get 模式)。
动手做视图里用 flash(消息, 类别) 存消息,模板里用 get_flashed_messages() 取出来渲染。注意:flash 依赖 session,必须设置 SECRET_KEY,否则会报错。
apps/views/web.pypython
from flask import Blueprint, flash, redirect, render_template, request, url_for
from werkzeug.security import check_password_hash
from ..models import User

web_bp = Blueprint("web", __name__)


@web_bp.post("/login")
def login():
    username = request.form.get("username", "")
    password = request.form.get("password", "")
    user = User.query.filter_by(username=username).first()

    if user is None or not check_password_hash(user.password, password):
        # 存一条「错误」类别的提示,随后重定向回登录页
        flash("用户名或密码错误", "error")
        return redirect(url_for("web.login_page"))

    flash("登录成功,欢迎回来", "success")
    return redirect(url_for("web.user_center"))


@web_bp.get("/login")
def login_page():
    # 渲染模板时,页面里会自动取出并显示上面 flash 的消息
    return render_template("login.html")
运行输出Python 运行结果
运行结果: # 提交错误密码 → 重定向回 /login,页面顶部显示: 用户名或密码错误 # 提交正确密码 → 重定向到个人中心,页面顶部显示: 登录成功,欢迎回来 # 关键:刷新页面后提示就消失了 —— 因为它只被消费一次
动手做模板里用 get_flashed_messages(with_categories=true) 取出「消息 + 类别」,再按类别渲染成不同颜色的提示条。这段通常放在布局模板里,所有页面就都能显示提示了。
templates/base.htmlhtml
{% with messages = get_flashed_messages(with_categories=true) %}
  {% if messages %}
    <ul class="flash-list">
      {% for category, message in messages %}
        <li class="flash flash-{{ category }}">{{ message }}</li>
      {% endfor %}
    </ul>
  {% endif %}
{% endwith %}

<!-- 渲染出来的 HTML(category 是 success,所以多了 flash-success 类):
     <ul class="flash-list">
       <li class="flash flash-success">登录成功,欢迎回来</li>
     </ul> -->
页面效果浏览器渲染结果
运行结果: 浏览器页面顶部出现一条绿色提示条: 登录成功,欢迎回来 CSS 里给 .flash-success / .flash-error 分别配绿、红两种底色即可: .flash-success { background:#e8f8ee; color:#1a7f43; } .flash-error { background:#fdeceb; color:#c0392b; }
讲透
  • 「闪现」的含义就是「读一次就消失」:get_flashed_messages() 在取出消息的同时会把它从 session 里清掉,所以刷新页面不会重复提示。
  • 类别(category)只是你自定义的标签:写成 "success"、"error"、"warning" 都行,Flask 不做限制,它只是原样交给你,由模板决定怎么显示。
  • 它和 session 是一套的:flash 把消息写进 session,而 session 要靠 SECRET_KEY 签名(防止用户篡改)。没配密钥就 flash,程序会直接抛异常。
  • 重要现实提醒:本项目是前后端分离,页面由 Vue 渲染,服务端不返回 HTML。所以 flash 在这套架构里用不上,提示信息改用统一响应的 msg 字段:{"code": 400, "msg": "用户名或密码错误"},前端拿到后弹一个 Toast。但 flash 是 Flask 的经典知识点,教材里有、面试也常问,必须理解它的用途与原理。
避坑两个高频错误:① 没设 SECRET_KEY 就调用 flash(),报 RuntimeError: The session is unavailable because no secret key was set;② 在 API 接口里用 flash 然后返回 jsonify,消息会一直留在 session 里没人消费,直到某次不小心被别的页面取出——在前后端分离项目里这就是个纯粹的 bug,直接改用 msg 字段。

钩子的执行顺序与注意事项

谁能拦住谁,谁先谁后
执行顺序:before 按注册,after 反着来 注册顺序 实际执行顺序 before A, before B after C, after D FIFO LIFO before A → before B after D → after C before_request 按注册先后(FIFO);after_request 像压栈弹栈,反着来(LIFO)。 多个钩子「进门排队、出门倒序」,别搞反了。

注册多个钩子时:before_request 按注册先后依次执行(FIFO),after_request 则是倒序执行(LIFO,像压栈弹栈)。记住「进门排队、出门倒序」就不会乱。

讲透:一次请求的完整时间线
  • before_request 按注册顺序执行(先注册的先跑);多个钩子只要有一个返回了响应,后面的 before_request 和视图函数全部跳过。
  • after_request 按注册顺序的「逆序」执行(先注册的后跑),最后注册的钩子最先动手——和「穿衣服 / 脱衣服」的顺序类似。
  • teardown_request 最后跑,且无论成功失败都会跑。
执行顺序示意text
请求进来
  │
  ├─ before_request #1   (注册顺序 →)
  ├─ before_request #2
  │      若其中某个返回了响应,就直接跳到 after_request,请求不进入视图
  │
  ├─ 视图函数 view()
  │      若视图抛异常 → 交给 errorhandler → 再进 after_request
  │
  ├─ after_request  #2   (← 注册逆序)
  ├─ after_request  #1
  │
  └─ teardown_request   (无论成功 / 失败都会执行)
效果预览运行结果
运行结果: 按上面顺序注册两个钩子后,访问任意接口,终端打印顺序是: before #1 before #2 (视图内的日志) after #2 after #1 teardown # 记住这句话就够了:before 正着走,after 倒着走,teardown 最后兜底
讲透:用钩子的三条纪律
  • 钩子是「横切关注点」的容器,不是业务逻辑的家:日志、鉴权、响应头、CORS 属于横切;「查作品列表」这种属于业务。别把业务塞进钩子。
  • 钩子里尽量别做慢操作:它是串行的,每个请求都要等它。在钩子里同步调用第三方 API 或做重查询,会拖垮全站响应时间。要用就异步、或加缓存。
  • 全局钩子要照顾到「例外接口」:比如统一鉴权钩子必须给登录、注册、作品列表留白名单,否则前端连登录都调不通——这是最容易出现的自锁 bug。
  • 钩子里同样能用 g 和 current_app:它运行在请求上下文中,这两个对象随时可用,这也是它能和视图共享数据的根本原因。
动手练习
1写一个 before_request 钩子,统计每个 IP 在一分钟内的请求次数;如果超过 60 次就返回 {"code": 429, "msg": "请求过于频繁,请稍后再试"}(简单的接口限流思路)。
2用 after_request 给所有响应加上 X-Powered-By: hiwen 头,然后访问任意接口,用 curl -i 确认它出现了。再想一想:为什么第 9 章的文件下载接口不该被统一响应包装钩子改坏?
3迁移练习:「红影赏析」需要(a)所有响应都带 CORS 头允许 http://localhost:5173;(b)记录每个请求的耗时并在超过 500ms 时输出警告日志。请用钩子实现这两点,并说明 为什么这两件事不该写在每个视图函数里。
参考答案
练习答案.pypython
# 第 1 题:简易限流
import time
from collections import defaultdict
from flask import g, request, jsonify

_hits = defaultdict(list)   # {ip: [时间戳, ...]}

@app.before_request
def rate_limit():
    now = time.time()
    ip = request.remote_addr
    # 只保留最近 60 秒内的记录
    _hits[ip] = [t for t in _hits[ip] if now - t < 60]
    if len(_hits[ip]) >= 60:
        return jsonify(code=429, msg="请求过于频繁,请稍后再试", data=None)
    _hits[ip].append(now)
    return None


# 第 2 题:加自定义响应头
@app.after_request
def add_powered_by(response):
    response.headers["X-Powered-By"] = "hiwen"
    return response
# 为什么下载接口不会被包装钩子改坏:
# 因为包装钩子里有 if not response.is_json: return response
# 图片/CSV 的响应 is_json 为 False,会原样放行


# 第 3 题:红影赏析
ALLOW_ORIGIN = "http://localhost:5173"

def register_middleware(app):
    @app.before_request
    def _start():
        g.start_time = time.perf_counter()

    @app.after_request
    def _cors_and_log(response):
        # (a) 统一 CORS
        response.headers["Access-Control-Allow-Origin"] = ALLOW_ORIGIN
        response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
        # (b) 耗时 + 慢请求告警
        cost_ms = (time.perf_counter() - g.start_time) * 1000
        if cost_ms > 500:
            app.logger.warning("慢请求 %s %s %.2fms",
                               request.method, request.path, cost_ms)
        return response
运行输出Python 运行结果
运行结果: 第 1 题:第 61 次请求返回 { "code": 429, "msg": "请求过于频繁,请稍后再试", "data": null } 第 2 题:curl -i 输出中出现 X-Powered-By: hiwen 第 3 题:慢请求时终端输出 [WARNING] 慢请求 GET /api/article/list 812.45ms # 其它请求静默通过(不打印),日志干净 # 为什么不该写进视图: # ① 几十个接口各写一遍 = 大量重复代码,改一处要改几十处 # ② 漏写某个视图就会出现「有的接口没跨域头」这类不一致 bug # ③ 它是「所有请求共有」的关注点,天然属于钩子

三题都指向同一个判断标准:「这件事是不是每个请求都要做?」 是——放钩子;不是——放视图。抓住这条,你就不会再纠结该写在哪了。

本章小结