中间件 Middleware

本文详细介绍了Django框架中的中间件机制,包括初始化方法、请求预处理、视图预处理、响应后处理及异常后处理等功能,展示了如何通过这些机制扩展Django的功能。

中间件方法 
Initializer: __init__(self) 
出于性能的考虑,每个已启用的中间件在每个服务器进程中只初始化 一 次。也就是说 __init__() 仅在服务进程启动的时候调用,而在针对单个request处理时并不执行。 
对一个middleware而言,定义 __init__() 方法的通常原因是检查自身的必要性。如果 __init__() 抛出异常 django.core.exceptions.MiddlewareNotUsed ,则Django将从middleware栈中移出该middleware。 
在中间件中定义 __init__() 方法时,除了标准的 self 参数之外,不应定义任何其它参数。 

Request预处理函数: process_request(self, request) 
这个方法的调用时机在Django接收到request之后,但仍未解析URL以确定应当运行的view之前。Django向它传入相应的 HttpRequest 对象,以便在方法中修改。 
process_request() 应当返回 None 或 HttpResponse 对象. 
如果返回 None , Django将继续处理这个request,执行后续的中间件, 然后调用相应的view. 
如果返回 HttpResponse 对象, Django 将不再执行 任何 其它的中间件(而无视其种类)以及相应的view。 Django将立即返回该 HttpResponse . 

View预处理函数: process_view(self, request, view, args, kwargs) 
这个方法的调用时机在Django执行完request预处理函数并确定待执行的view之后,但在view函数实际执行之前。 
request HttpRequest 对象 . 
view Django将调用的处理request的python函数. 这是实际的函数对象本身, 而不是字符串表述的函数名。 
args 将传入view的位置参数列表,但不包括 request 参数(它通常是传入view的第一个参数) 
kwargs 将传入view的关键字参数字典. 
如同 process_request() , process_view() 应当返回 None 或 HttpResponse 对象。 
如果返回 None , Django将继续处理这个 request ,执行后续的中间件, 然后调用相应的view.luxi78@gmail.com 
如果返回 HttpResponse 对象, Django 将不再执行 任何 其它的中间件(不论种类)以及相应的view. Django将立即返回 

Response后处理函数: process_response(self, request, response) 
这个方法的调用时机在Django执行view函数并生成response之后。这里,该处理器就能修改response的内容;一个常见的用途是内容压缩,如gzip所请求的HTML页面。 
这个方法的参数相当直观: request 是request对象,而 response 则是从view中返回的response对象。 
process_response() 必须 返回 HttpResponse 对象. 这个response对象可以是传入函数的那一个原始对象(通常已被修改),也可以是全新生成的。 

Exception后处理函数: process_exception(self, request, exception) 
这个方法只有在request处理过程中出了问题并且view函数抛出了一个未捕获的异常时才会被调用。这个钩子可以用来发送错误通知,将现场相关信息输出到日志文件, 或者甚至尝试从错误中自动恢复。 
这个函数的参数除了一贯的 request 对象之外,还包括view函数抛出的实际的异常对象 exception 。 
process_exception() 应当返回 None 或 HttpResponse 对象. 
如果返回 None , Django将用框架内置的异常处理机制继续处理相应request。 
如果返回 HttpResponse 对象, Django 将使用该response对象,而短路框架内置的异常处理机制

<think>我们计划通过自定义中间件来实现统一响应体,该中间件将修改响应内容,添加一个包含时间戳和请求URL的head字段。 注意:中间件处理的是响应,但我们需要在中间件中获取请求的URL,并在生成响应体时使用。 然而,由于我们想要修改响应体结构(而不仅仅是添加响应头),我们需要在中间件中捕获响应内容,然后重新封装。 步骤: 1. 创建一个中间件,它将在请求被处理前记录开始时间(如果需要更精确的时间,可以在调用前记录时间戳,但注意请求处理时间可能较长,我们这里使用处理完成时的时间戳作为响应时间戳?) 2. 在中间件中调用`call_next`得到响应。 3. 然后解析响应内容,将其作为`body`,同时构建`head`(包含请求URL和当前时间戳)。 4. 将新的响应体(包含body和head)序列化为JSON返回。 但是注意:这种方式可能会影响流式响应和文件下载等,所以只适用于JSON响应。 另一种方案:我们不在中间件中处理,而是使用依赖注入(如之前的方案)。但用户要求用中间件。 权衡:中间件可以全局处理,但需要谨慎处理不同类型的响应(比如文件流、重定向等)。这里我们假设所有响应都是JSON。 实现: 定义响应模型(在中间件中构造): { "body": 原始响应的内容, "head": { "request_url": str(request.url), "timestamp": datetime.utcnow().isoformat() } } 注意:中间件应该跳过非200的响应吗?或者我们想统一所有响应(包括错误)?这里我们统一处理所有响应,包括错误。 但是,错误响应可能已经是JSON结构,我们需要将其也作为body。例如,原本返回的是`{"detail": "Not found"}`,那么现在变成: { "body": {"detail": "Not found"}, "head": {...} } 步骤: 1. 创建中间件类,继承自`BaseHTTPMiddleware`(需要安装`starlette`,因为FastAPI自带)。 2. 在`dispatch`方法中: - 记录请求开始时间(如果需要计算处理时间,但这里我们只需要响应时间戳,所以用结束时间) - 调用`call_next`得到响应 - 检查响应头中的`Content-Type`,如果是`application/json`,则处理 - 读取响应体(注意:响应体是字节串) - 解析为JSON,然后封装 - 重新设置响应体(注意:保持状态码不变) 但是,非JSON响应怎么办?我们可以选择只处理JSON响应,其他的跳过。 另一种做法:强制所有响应都转为这种结构?但可能不适合文件下载。 因此,我们可以通过一个条件来决定是否封装,例如只封装那些`Content-Type`为`application/json`的响应。 具体步骤: 1. 获取响应:`response = await call_next(request)` 2. 检查`response.headers.get('content-type')`是否为`application/json` 3. 如果是,则读取响应体,解析JSON,然后封装,再设置回响应体(注意:响应体长度可能改变,需要更新`Content-Length`或移除该头) 4. 如果不是,则直接返回响应。 注意:我们还需要记录时间戳(使用处理完成时的时间)和请求的URL。 代码示例: ```python import json from datetime import datetime from fastapi import FastAPI, Request from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import JSONResponse app = FastAPI() class CommonResponseMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # 先调用视图函数得到响应 response = await call_next(request) # 只处理JSON响应 if response.headers.get('content-type') == 'application/json': # 获取原始响应体 body_bytes = await response.body() # 解析原始JSON try: body_json = json.loads(body_bytes) except json.JSONDecodeError: # 如果解析失败,则直接返回原响应 return response # 构建新的响应体 new_body = { "body": body_json, "head": { "request_url": str(request.url), "timestamp": datetime.utcnow().isoformat() } } # 创建新的JSONResponse,保持原状态码和头部(但需要移除content-length,因为内容变了) headers = dict(response.headers) headers.pop('content-length', None) # 移除content-length,让Starlette重新计算 return JSONResponse( content=new_body, status_code=response.status_code, headers=headers ) return response app.add_middleware(CommonResponseMiddleware) # 示例路由 @app.get("/") async def root(): return {"message": "Hello World"} @app.get("/error") async def error(): return JSONResponse(status_code=404, content={"detail": "Not found"}) ``` 注意:这里我们使用了`await response.body()`来获取原始响应体,这会读取整个响应体到内存。对于大文件响应可能不合适,但我们只处理JSON响应,通常不会太大。 另外,我们移除了`content-length`头,因为响应体大小改变了,由Starlette自动计算新的长度。 但是,错误处理:如果原始响应已经是我们的统一结构(比如在依赖注入中已经处理),那么中间件会再次封装吗?这样会导致嵌套。所以,我们应确保只使用一种方式(要么中间件,要么依赖注入),不要同时使用。 这个中间件会捕获所有JSON响应,包括错误响应(如404返回的JSON),所以统一了结构。 测试: - 访问 `/` 返回: { "body": {"message": "Hello World"}, "head": { "request_url": "http://localhost:8000/", "timestamp": "2023-10-05T09:00:00.123456" } } - 访问 `/error` 返回: { "body": {"detail": "Not found"}, "head": {...}, "status_code": 404 } 注意:状态码在响应的状态行中,而统一结构中的status_code字段我们并没有添加,因为状态码已经在HTTP响应中体现了。所以我们的统一结构只有body和head。 但是,上面的代码在错误响应中返回的也是统一结构,状态码为404,但内容体被封装了。 这样前端就可以统一处理响应结构了。 注意:如果某些中间件已经修改了响应(比如添加了CORS头),我们的中间件在最后处理,所以不会影响。 但是,中间件的执行顺序很重要。我们的中间件应该在其他中间件之后执行(因为要处理响应),但实际上,中间件是按照添加顺序的逆序执行响应的(即先添加的中间件,它的dispatch方法中`call_next`会调用后面的中间件,所以响应时是从内层中间件往外层返回)。因此,我们添加的这个中间件应该尽可能早地添加,这样它就能在最内层处理响应(这样能确保我们处理的是最终的响应,而不是被其他中间件修改过的)。但实际上,我们这里只依赖请求和响应,所以放在哪里都可以,只要在生成响应的中间件之后处理即可。但通常,我们可以在添加其他中间件之前添加这个中间件。 另一种做法:使用FastAPI的`app.middleware('http')`装饰器,但这里我们使用`BaseHTTPMiddleware`类。 注意:`BaseHTTPMiddleware`需要`starlette`,但FastAPI已经包含了。 关于时间戳:我们使用UTC时间,并格式化为ISO字符串。 如果不想处理非200的响应,可以在中间件中检查`response.status_code`,但这里我们处理所有JSON响应。 另外,如果原始响应不是JSON,比如是纯文本,我们也可以选择将其作为字符串放在body中,但这样会破坏结构(因为body可能是任意类型)。所以这里我们只处理JSON响应。 如果需要处理纯文本,可以扩展中间件,将纯文本也封装,例如: ```python if content_type in ['application/json', 'text/plain']: # 如果是纯文本,则把响应体作为字符串 body_text = body_bytes.decode() if content_type == 'application/json': body_data = json.loads(body_text) else: body_data = body_text ... # 然后封装 ``` 但这里我们按需求只处理JSON。 最后,注意性能:每次请求都会解析JSON和重新序列化,增加开销。 因此,如果性能要求很高,可以考虑其他方式(如依赖注入,在路由函数中直接返回统一结构,这样没有中间件解析的开销)。 但是,中间件的好处是全局统一处理,无需每个路由修改。 根据需求,我们提供中间件方案。 相关问题: 1. 如何为 FastAPI 中间件编写单元测试? 2. 如何处理中间件中的异常? 3. 如何将中间件应用于特定的路由? 4. 中间件中如何记录请求处理时间(即计算请求耗时)? 5. 如何避免中间件对某些路由(如健康检查)的响应进行封装? 我们可以在中间件中通过请求路径来判断是否跳过,例如: ```python if request.url.path in ["/health", "/metrics"]: return await call_next(request) ``` 或者在中间件中设置一个跳过列表。 以上代码实现了通过中间件统一响应体的功能。</think># FastAPI 自定义中间件实现统一响应体 在 FastAPI 中,使用自定义中间件是实现统一响应体的高效方式。下面我将详细介绍如何创建中间件来添加包含时间戳和请求 URL 的统一响应头。 ## 完整实现方案 ```python from datetime import datetime import json from fastapi import FastAPI, Request from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import JSONResponse app = FastAPI() class UnifiedResponseMiddleware(BaseHTTPMiddleware): """统一响应体中间件""" async def dispatch(self, request: Request, call_next): # 记录请求开始时间(用于计算处理时间) start_time = datetime.utcnow() # 继续处理请求 response = await call_next(request) # 只处理JSON响应 if response.headers.get("content-type") == "application/json": # 获取原始响应内容 response_body = await response.body() original_content = json.loads(response_body) if response_body else {} # 构建统一响应结构 unified_response = { "body": original_content, "head": { "request_url": str(request.url), # 请求完整URL "timestamp": datetime.utcnow().isoformat(), # 响应时间戳 "processing_time": f"{(datetime.utcnow() - start_time).total_seconds() * 1000:.2f}ms" # 处理耗时 } } # 创建新响应并保持原始状态码 return JSONResponse( content=unified_response, status_code=response.status_code, headers=dict(response.headers) ) return response # 注册中间件 app.add_middleware(UnifiedResponseMiddleware) # 示例路由 @app.get("/items") async def get_items(): """获取项目列表""" return {"items": [{"id": 1, "name": "Item A"}, {"id": 2, "name": "Item B"}]} @app.get("/error") async def simulate_error(): """模拟错误响应""" return JSONResponse( content={"error": "Not found"}, status_code=404 ) ``` ## 关键实现说明 1. **中间件类设计**: - 继承 `BaseHTTPMiddleware` 实现自定义中间件 - `dispatch` 方法处理请求/响应流程 - 记录请求开始时间用于计算处理耗时 2. **统一响应结构**: ```python { "body": {...}, # 原始业务数据 "head": { "request_url": "http://example.com/items", "timestamp": "2023-10-05T09:30:15.123456Z", "processing_time": "12.34ms" } } ``` 3. **处理流程**: - 只处理 `application/json` 类型的响应 - 保留原始响应的状态码和头部信息 - 添加请求 URL 和 UTC 时间戳(ISO 8601 格式) - 计算并添加请求处理耗时 ## 响应示例 **成功响应 (GET /items)**: ```json { "body": { "items": [ {"id": 1, "name": "Item A"}, {"id": 2, "name": "Item B"} ] }, "head": { "request_url": "http://localhost:8000/items", "timestamp": "2023-10-05T09:30:15.123456Z", "processing_time": "5.23ms" } } ``` **错误响应 (GET /error)**: ```json { "body": { "error": "Not found" }, "head": { "request_url": "http://localhost:8000/error", "timestamp": "2023-10-05T09:31:22.456789Z", "processing_time": "3.12ms" } } ``` ## 高级配置选项 ### 1. 添加请求ID跟踪 ```python import uuid # 在中间件中添加 request_id = str(uuid.uuid4()) # ... unified_response["head"]["request_id"] = request_id ``` ### 2. 排除特定路由 ```python # 在dispatch方法中添加 excluded_paths = ["/docs", "/redoc", "/openapi.json"] if request.url.path in excluded_paths: return await call_next(request) ``` ### 3. 支持Unix时间戳格式 ```python # 替换timestamp生成 "timestamp": datetime.utcnow().timestamp() # 返回Unix时间戳 ``` ## 性能考虑 1. **选择性处理**:只处理JSON响应,避免影响静态文件等非API请求 2. **内存优化**:使用流式处理处理大响应体 3. **异步处理**:确保中间件逻辑不阻塞事件循环 这种中间件实现方式提供了统一、可扩展的响应结构,同时保持了FastAPI的高性能和灵活性[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值