Fastapi框架-冷饭再炒-基础知识补充篇(5)- 自定义中间件,在中间获取响应体报文
Fastapi 自定义中间件
这里的中间件主要作用其实就是在请求前和请求后处理机制。通常我们的可以在中间件里处理的事情有:
- 日志记录
- 鉴权
- 数据库的操作开关处理等
如在之前的Flask中其实是比较简单的,如flask中几个钩子的函数:
- before_first_request:第一个请求运行前
- before_request:每次请求前运行。
- after_request:处理逻辑没有异常抛出,每次请求后运行(这里可以返回我们的自定义的响应体)
- teardown_request:在每次请求后运行,即使处理发生了错误。
- teardown_appcontext:在应用上下文从栈中弹出之前运行
但是在fastapi中的处理没有类似上述的函数的回调。它的中间件类似的我们GO语言的gin中间件一样。以下是我自己对中间件使用过程中遇到的一些问题的整理。
1:最简的http请求类型中间件
如官网提供的最简自定义的中间件示例:
import time
from typing import Callable
from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.routing import APIRoute
from starlette.responses import StreamingResponse, JSONResponse
import json
app = FastAPI()
@app.middleware("http")
async def log_request(request, call_next):
print('请求开始前我可以处理事情11111')
response = await call_next(request)
print('请求开始后我可以处理的事情33333333333')
return response
@app.get("/")
async def not_timed():
print('请求开始后我可以处理的事情222222')
return {"message": "你好"}
import uvicorn
if __name__ == '__main__':
# 等于通过 uvicorn 命令行 uvicorn 脚本名:app对象 启动服务:uvicorn xxx:app --reload
uvicorn.run('main:app', host="127.0.0.1", port=8000, debug=True, reload=True)
输出结果为:
请求开始前我可以处理事情11111
请求开始后我可以处理的事情222222
请求开始后我可以处理的事情33333333333
INFO: 127.0.0.1:1084 - "GET / HTTP/1.1" 200 OK
2:基于BaseHTTPMiddleware的实现的中间件
多自定义中间件示例:
import time
from typing import Callable
from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.routing import APIRoute
from starlette.responses import StreamingResponse, JSONResponse
import json
from starlette.middleware.base import BaseHTTPMiddleware
app = FastAPI()
@app.middleware("http")
async def log_request(request, call_next):
print('请求开始前我可以处理事情11111')
response = await call_next(request)
print('请求开始后我可以处理的事情33333333333')
return response
# 基于BaseHTTPMiddleware的中间件实例,
class CostimeHeaderMiddleware(BaseHTTPMiddleware):
# dispatch 必须实现
async def dispatch(self, request, call_next):
print('请求开始前我可以处理事情44444444444444')
start_time = time.time()
responser = await call_next(request)
process_time = round(time.time() - start_time, 4)
# 返回接口响应时间
responser.headers["X-Process-Time"] = f"{process_time} (s)"
print('请求开始前我可以处理事情555555555')
return responser
# 基于BaseHTTPMiddleware的中间件实例,
class CostimeHeaderMiddleware2(BaseHTTPMiddleware):
# dispatch 必须实现
async def dispatch(self, request, call_next):
print('请求开始前我可以处理事情666666666666666666666')
start_time = time.time()
responser = await call_next(request)
process_time = round(time.time() - start_time, 4)
# 返回接口响应时间
responser.headers["X-Process-Time"] = f"{process_time} (s)"
print('请求开始前我可以处理事情7777777777777777777777')
return responser
app.add_middleware(CostimeHeaderMiddleware)
app.add_middleware(CostimeHeaderMiddleware2)
@app.get("/")
async def not_timed():
print('请求开始后我可以处理的事情222222')
return {"message": "你好"}
import uvicorn
if __name__ == '__main__':
# 等于通过 uvicorn 命令行 uvicorn 脚本名:app对象 启动服务:uvicorn xxx:app --reload
uvicorn.run('main2:app', host="127.0.0.1", port=8000, debug=True, reload=True)
输出结果为:
请求开始前我可以处理事情666666666666666666666
请求开始前我可以处理事情44444444444444
请求开始前我可以处理事情11111
请求开始后我可以处理的事情222222
请求开始后我可以处理的事情33333333333
请求开始前我可以处理事情555555555
请求开始前我可以处理事情7777777777777777777777
INFO: 127.0.0.1:1998 - "GET / HTTP/1.1" 200 OK
PS1:从上面的输出结果可以看得到我们的中间件的注册顺序非常的重要。他们的上面的注册顺序是:
---->越是最晚注册的,它就会在中间件的洋葱模型的最外层。
---->越是最早注册的,它就会在中间件的洋葱模型的最内层。
PS2:假如你有需要使用中间件来处理全局异常的捕获的话,则当然是放在最外层去处理咯!
3:中间件中获取最终responser返回值
通常有上面的需求的话,一般是放在我们的日志记录中,请求完成后,我们的日志记录我们返回给客户端的东西,这时候,就需要在中间里获取到我们的最终的响应体的报文内容。
但是是打印出来看我们的responser,你会发现它是一个:
- <starlette.responses.StreamingResponse object at 0x0000020C287EABA8>
打印 print(response.dict): 你会打得到下面的信息:
{'body_iterator': <async_generator object BaseHTTPMiddleware.call_next.<locals>.body_stream at 0x0000020C2886B6A8>, 'status_code': 200, 'media_type': None, 'background': None, 'raw_headers': [(b'content-length', b'20'), (b'content-type', b'application/json')]}
我们发现好像根本无法直接的获取到我们的响应报文信息,从上面可以看得出我们的响应报文应该是在async_generator object BaseHTTPMiddleware这个异步生成器里面了!
翻看官方提的issues,也有比人遇到类似的需求询问
[QUESTION] How to get Response Body from middleware #954
https://github.com/tiangolo/fastapi/issues/954
不过翻看了一下,最终没有一个满意的答案。
然后不经意间在stackoverflow.com 翻看到另一个关于这个的需求的实现,有一个老铁是已经给出了就具体方案: 具体地址为:stackoverflow.com/questions/6… 已下是完整的示例:
#!/usr/bin/evn python
# coding=utf-8
# + + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + +
# ┏┓ ┏┓+ +
# ┏┛┻━━━┛┻┓ + +
# ┃ ┃
# ┃ ━ ┃ ++ + + +
# ████━████ ┃+
# ┃ ┃ +
# ┃ ┻ ┃
# ┃ ┃ + +
# ┗━┓ ┏━┛
# ┃ ┃
# ┃ ┃ + + + +
# ┃ ┃ Codes are far away from bugs with the animal protecting
# ┃ ┃ + 神兽保佑,代码无bug
# ┃ ┃
# ┃ ┃ +
# ┃ ┗━━━┓ + +
# ┃ ┣┓
# ┃ ┏┛
# ┗┓┓┏━┳┓┏┛ + + + +
# ┃┫┫ ┃┫┫
# ┗┻┛ ┗┻┛+ + + +
# + + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + ++ + + +"""
"""
Author = zyx
@version: v1.0.0
@File: __init__.py.py
@文件功能描述:------
"""
import time
from typing import Callable
from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.routing import APIRoute
from starlette.responses import StreamingResponse, JSONResponse
import json
app = FastAPI()
class aiwrap:
def __init__(self, obj):
self._it = iter(obj)
def __aiter__(self):
return self
async def __anext__(self):
try:
value = next(self._it)
except StopIteration:
raise StopAsyncIteration
return value
@app.middleware("http")
async def log_request(request, call_next):
print('请求开始前我可以处理事情11111')
response = await call_next(request)
print('请求开始后我可以处理的事情33333333333',response)
resp_body = [section async for section in response.__dict__['body_iterator']]
# Repairing FastAPI response
response.__setattr__('body_iterator', aiwrap(resp_body))
# Formatting response body for logging
try:
resp_body = json.loads(resp_body[0].decode())
except:
resp_body = str(resp_body)
print("中间件里面获取到最终返回的响应体的信息", resp_body)
print(response.__dict__)
return response
@app.get("/")
async def not_timed():
return {"message": "Not timed"}
import uvicorn
if __name__ == '__main__':
# 等于通过 uvicorn 命令行 uvicorn 脚本名:app对象 启动服务:uvicorn xxx:app --reload
uvicorn.run('main:app', host="127.0.0.1", port=8000, debug=True, reload=True)
输出结果是:
请求开始前我可以处理事情11111
请求开始后我可以处理的事情33333333333 <starlette.responses.StreamingResponse object at 0x0000026BA8369860>
中间件里面获取到最终返回的响应体的信息 {'message': 'Not timed'}
{'body_iterator': <main.aiwrap object at 0x0000026BA846B780>, 'status_code': 200, 'media_type': None, 'background': None, 'raw_headers': [(b'content-length', b'23'), (b'content-type', b'application/json')]}
INFO: 127.0.0.1:5674 - "GET / HTTP/1.1" 200 OK
4:取巧方式,通过 req.state.来写入响应报文,然后在中间里读取
因为我们的响应的报文是在中间里面内层执行的,所以我们的可以对我们的请求体里面写入我们的响应报文即可,不过这种方式可能需要在我们的ApiResponse(JSONResponse)里面进行对于的Request进行传递才可以仅供参考!
import time
from typing import Callable
from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.routing import APIRoute
from starlette.responses import StreamingResponse, JSONResponse
import json
from typing import Optional,Dict,Any
app = FastAPI()
@app.middleware("http")
async def log_request(request: Request, call_next):
print('请求开始前我可以处理事情11111')
response = await call_next(request)
print('请求开始后我可以处理的事情33333333333', response)
# Repairing FastAPI response
print("另一种取巧的方式:中间件里面获取到最终返回的响应体的信息", request.state.rspbody)
print(response.__dict__)
return response
class ApiResponse(JSONResponse):
# 定义返回响应码--如果不指定的话则默认都是返回200
http_status_code = 200
# 默认成功
code = 0
# 默认Node.如果是必选的,去掉默认值即可
data: Optional[Dict[str, Any]] = None # 结果可以是{} 或 []
msg = '成功'
def __init__(self, req: Request = None, http_status_code=None, code=None, data=None, msg=None, **options):
if data:
self.data = data
if msg:
self.msg = msg
if code:
self.code = code
if http_status_code:
self.http_status_code = http_status_code
# 返回内容体
body = dict(
msg=self.msg,
code=self.code,
data=self.data,
)
if req:
pass
req.state.rspbody = body
super(ApiResponse, self).__init__(status_code=self.http_status_code, content=body, **options)
@app.get("/")
async def not_timed(req: Request):
return ApiResponse(req=req)
import uvicorn
if __name__ == '__main__':
# 等于通过 uvicorn 命令行 uvicorn 脚本名:app对象 启动服务:uvicorn xxx:app --reload
uvicorn.run('main:app', host="127.0.0.1", port=8000, debug=True, reload=True)
输出的结果为:
请求开始前我可以处理事情11111
请求开始后我可以处理的事情33333333333 <starlette.responses.StreamingResponse object at 0x0000029C640835C0>
另一种取巧的方式:中间件里面获取到最终返回的响应体的信息 {'msg': '成功', 'code': 0, 'data': None}
{'body_iterator': <async_generator object BaseHTTPMiddleware.call_next.<locals>.body_stream at 0x0000029C6407F598>, 'status_code': 200, 'media_type': None, 'background': None, 'raw_headers': [(b'content-length', b'37'), (b'content-type', b'application/json')]}
INFO: 127.0.0.1:1307 - "GET / HTTP/1.1" 200 OK
结尾
简单小笔记!仅供参考!
转载自:https://juejin.cn/post/6971451349141553165