中间件与错误处理 · 后端开发
怎么给每个路由都加上日志和错误处理,又不用重复写?
课程目录为什么需要它
每个处理函数都需要同样的额外工作:记录请求、计时、把崩溃变成干净的 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')
- L9相当于在 def 之后写 home = logged(home)。
- L13这里调用的是 wrapper,而不是原来的 home。
- L3在处理函数之前。
-> / - L4原来的处理函数在这里运行。
- 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
- L1装饰器工厂:layer("A") 返回一个装饰器。
- L11最后应用,所以是最外层。
- 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']
- L7处理函数里的任何崩溃都会落到这里。
- L8把细节留给开发者。
- L9客户端只知道出了问题。
- L18除以 len([]) = 0 会抛出 ZeroDivisionError,变成 500。
常见错误
忘了返回响应
wrapper 调用了 handler(request) 却没有 return,每个路由都会返回 None。
泄露错误细节
return 500, {"error": str(e)} 可能把 SQL、文件路径或密钥展示给任何人。细节记日志;返回通用消息。
装饰器的顺序
最上面的装饰器在最外层。如果希望崩溃也带着状态码被记录,就把错误处理放在日志外面。
小测验
1. 写在 `def home` 上面的 `@logged` 做了什么?
查看答案
答案: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"))查看答案
答案:A
exclaim 先包住 greet("hi ann!"),然后 shout 把整个结果变成大写。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出跟着层走
准确写出它打印的内容。
- 改需求别再泄露错误
这个错误中间件把异常信息发给了客户端。修改它:客户端只得到
500{"error": "internal error"},细节"<异常类型>: <信息>"改为追加到LOG里。 - 从零写必须是 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. |