人人都能学编程
English 开始学习
专项 · 后端第 5 课 / 共 6 课免费阅读

中间件与错误处理 · 后端开发

怎么给每个路由都加上日志和错误处理,又不用重复写?

课程目录

为什么需要它

每个处理函数都需要同样的额外工作:记录请求、计时、把崩溃变成干净的 500、检查登录。把这些复制进五十个处理函数,bug 和漏洞就是这么来的。中间件只需写一次就能包住所有处理函数,每个框架都有它:Flask 和 FastAPI 里的装饰器,Express 的 app.use,Spring 的过滤器。

直觉理解

装饰器是一个函数:它接收一个处理函数,返回一个新的处理函数,在调用原函数的前后做点事:

def logged(handler):
    def wrapper(request):
        print("->", request["path"])
        response = handler(request)
        print("<-", response[0])
        return response
    return wrapper

@logged
def home(request):
    return 200, "welcome"

写在 def home 上面的 @logged,意思就是 home = logged(home)。现在调用 home(...) 实际运行的是 wrapper,由它去调用原函数。

叠加起来,它们像洋葱一样一层套一层:@a 写在 @b 上面时,请求先经过 a,再经过 b,然后到处理函数,响应再从 b、a 一层层出来。

错误中间件捕获任何异常,把细节记到日志里给你看,只给客户端返回一个简单的 500 {"error": "internal error"}。永远不要把异常信息或调用栈发给客户端:它们可能暴露文件路径、SQL 和密钥。

定义

中间件

Code that runs around every handler: logging, timing, auth, error handling.
包在每个处理函数外面运行的代码:日志、计时、鉴权、错误处理。

装饰器

A function that takes a function and returns a wrapped version. @name above def applies it.
接收一个函数、返回其包装版本的函数。写在 def 上面的 @名字 就是应用它。

示例

日志装饰器

动手试试

wrapper 包在处理函数外面运行。

def logged(handler):
    def wrapper(request):
        print("->", request["path"])
        response = handler(request)
        print("<-", response[0])
        return response
    return wrapper

@logged
def home(request):
    return 200, "welcome"

result = home({"path": "/"})
print(result)
输出
-> /
<- 200
(200, 'welcome')
逐步执行
  1. L9相当于在 def 之后写 home = logged(home)。
  2. L13这里调用的是 wrapper,而不是原来的 home。
  3. L3在处理函数之前。-> /
  4. L4原来的处理函数在这里运行。
  5. L5在处理函数之后。<- 200

洋葱的一层层

动手试试

两个装饰器:从外层进去,最后从外层出来。

def layer(name):
    def decorate(handler):
        def wrapper(request):
            print("in", name)
            response = handler(request)
            print("out", name)
            return response
        return wrapper
    return decorate

@layer("A")
@layer("B")
def hello(request):
    print("handler")
    return 200, "hi"

hello({})
输出
in A
in B
handler
out B
out A
逐步执行
  1. L1装饰器工厂:layer("A") 返回一个装饰器。
  2. L11最后应用,所以是最外层。
  3. L17in A、in B、handler、out B、out A。

错误变成干净的 500

动手试试

客户端得到一条简单的消息;细节记进你的日志。

LOG = []

def catch_errors(handler):
    def wrapper(request):
        try:
            return handler(request)
        except Exception as e:
            LOG.append(f"{request['path']}: {type(e).__name__}: {e}")
            return 500, {"error": "internal error"}
    return wrapper

@catch_errors
def average(request):
    nums = request["nums"]
    return 200, {"avg": sum(nums) / len(nums)}

print(average({"path": "/avg", "nums": [2, 4]}))
print(average({"path": "/avg", "nums": []}))
print(LOG)
输出
(200, {'avg': 3.0})
(500, {'error': 'internal error'})
['/avg: ZeroDivisionError: division by zero']
逐步执行
  1. L7处理函数里的任何崩溃都会落到这里。
  2. L8把细节留给开发者。
  3. L9客户端只知道出了问题。
  4. L18除以 len([]) = 0 会抛出 ZeroDivisionError,变成 500。

常见错误

忘了返回响应

wrapper 调用了 handler(request) 却没有 return,每个路由都会返回 None。

泄露错误细节

return 500, {"error": str(e)} 可能把 SQL、文件路径或密钥展示给任何人。细节记日志;返回通用消息。

装饰器的顺序

最上面的装饰器在最外层。如果希望崩溃也带着状态码被记录,就把错误处理放在日志外面。

小测验

1. 写在 `def home` 上面的 `@logged` 做了什么?

  1. Ahome = logged(home)
  2. B每次加载文件都调用 logged
  3. C把 home 的源代码记进日志
查看答案

答案:A

装饰器用它返回的东西替换掉原函数。

2. 这段代码会输出什么?

def shout(handler):
    def wrapper(x):
        return handler(x).upper()
    return wrapper

def exclaim(handler):
    def wrapper(x):
        return handler(x) + "!"
    return wrapper

@shout
@exclaim
def greet(name):
    return "hi " + name

print(greet("ann"))
  1. A
    HI ANN!
  2. B
    HI ANN
  3. C
    hi ann!
查看答案

答案:A

exclaim 先包住 greet("hi ann!"),然后 shout 把整个结果变成大写。

动手练习

3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。

  1. 预测输出跟着层走

    准确写出它打印的内容。

  2. 改需求别再泄露错误

    这个错误中间件把异常信息发给了客户端。修改它:客户端只得到 500 {"error": "internal error"},细节 "<异常类型>: <信息>" 改为追加到 LOG 里。

  3. 从零写必须是 JSON

    写一个装饰器 require_json(handler)。wrapper 收到的 request 字典里有 "headers" 和 "body":

    • 如果 request["headers"].get("Content-Type") 不是 "application/json" → 返回 (415, {"error": "expected JSON"})
    • 如果 body 不是合法的 JSON → 返回 (400, {"error": "invalid JSON"})
    • 否则把解析后的 body 放进 request["json"],返回 handler(request)

在互动课里练这一课

打开互动课,这些例子都能直接改、直接运行,练习题提交马上判题。免费开始,不用注册。

互动课按顺序解锁:前面的课还没学,会先带你到课程地图。

本课术语

术语中文英文定义
Middleware中间件Code that wraps every request: logging, auth, errors.
Decorator装饰器@name above a def: replaces the function with a wrapped version.