refactor: 迁移中间件到 middleware 模块
创建 outlook_web/middleware/ 目录,从 legacy.py 迁移: 新增文件: - middleware/__init__.py - 模块导出 - middleware/trace.py - trace_id 中间件 - ensure_trace_id(): 为每个请求生成/透传 trace_id - attach_trace_id_and_normalize_errors(): 统一写入 X-Trace-Id,标准化错误格式 - middleware/error_handler.py - 错误处理中间件 - handle_http_exception(): 处理 HTTP 异常 - handle_exception(): 处理未捕获异常 更新文件: - app.py: 从 middleware 模块导入,替代 legacy 引用 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f31f042d89
commit
c0562ef721
4 files changed
+200
-5
No files matched your search
+11
-5
@@ -24,6 +24,12 @@ def create_app(*, autostart_scheduler: Optional[bool] = None):
|
||||
from outlook_web import config, legacy
|
||||
from outlook_web.db import register_db
|
||||
from outlook_web.security.csrf import init_csrf
|
||||
from outlook_web.middleware import (
|
||||
ensure_trace_id,
|
||||
attach_trace_id_and_normalize_errors,
|
||||
handle_http_exception,
|
||||
handle_exception,
|
||||
)
|
||||
from outlook_web.routes import (
|
||||
accounts,
|
||||
audit,
|
||||
@@ -62,11 +68,11 @@ def create_app(*, autostart_scheduler: Optional[bool] = None):
|
||||
# CSRF(可选)
|
||||
_csrf, csrf_exempt, _generate_csrf = init_csrf(app)
|
||||
|
||||
# trace_id + error 结构标准化(复用 legacy 的 hooks 逻辑,保持契约一致)
|
||||
app.before_request(legacy.ensure_trace_id)
|
||||
app.after_request(legacy.attach_trace_id_and_normalize_errors)
|
||||
app.register_error_handler(HTTPException, legacy.handle_http_exception)
|
||||
app.register_error_handler(Exception, legacy.handle_exception)
|
||||
# trace_id + error 结构标准化(迁移到 middleware 模块)
|
||||
app.before_request(ensure_trace_id)
|
||||
app.after_request(attach_trace_id_and_normalize_errors)
|
||||
app.register_error_handler(HTTPException, handle_http_exception)
|
||||
app.register_error_handler(Exception, handle_exception)
|
||||
|
||||
# Blueprint 路由注册(URL 不变)
|
||||
app.register_blueprint(pages.create_blueprint(csrf_exempt=csrf_exempt))
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Middleware module
|
||||
from outlook_web.middleware.trace import (
|
||||
ensure_trace_id,
|
||||
attach_trace_id_and_normalize_errors,
|
||||
)
|
||||
from outlook_web.middleware.error_handler import (
|
||||
handle_http_exception,
|
||||
handle_exception,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ensure_trace_id",
|
||||
"attach_trace_id_and_normalize_errors",
|
||||
"handle_http_exception",
|
||||
"handle_exception",
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
错误处理中间件
|
||||
|
||||
功能:
|
||||
- 处理可预期的 HTTP 异常
|
||||
- 处理未捕获的异常
|
||||
- 返回统一错误结构
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import g, jsonify, request
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from outlook_web.errors import build_error_payload
|
||||
|
||||
|
||||
def handle_http_exception(error: HTTPException):
|
||||
"""处理可预期的 HTTP 异常,返回统一错误结构(仅对 API/JSON 请求返回 JSON)"""
|
||||
status_code = error.code or 500
|
||||
|
||||
message_map = {
|
||||
400: "请求参数错误",
|
||||
401: "未授权",
|
||||
403: "无权限",
|
||||
404: "资源不存在",
|
||||
405: "请求方法不被允许",
|
||||
429: "请求过于频繁,请稍后再试",
|
||||
}
|
||||
message = message_map.get(status_code, "请求失败")
|
||||
|
||||
trace_id_value = None
|
||||
try:
|
||||
trace_id_value = getattr(g, 'trace_id', None)
|
||||
except Exception:
|
||||
trace_id_value = None
|
||||
|
||||
error_payload = build_error_payload(
|
||||
code="HTTP_ERROR",
|
||||
message=message,
|
||||
err_type="HttpError",
|
||||
status=status_code,
|
||||
details=str(error),
|
||||
trace_id=trace_id_value
|
||||
)
|
||||
|
||||
if request.path.startswith('/api/') or request.is_json:
|
||||
return jsonify({'success': False, 'error': error_payload}), status_code
|
||||
|
||||
return f"{message} (trace_id={error_payload.get('trace_id')})", status_code
|
||||
|
||||
|
||||
def handle_exception(error):
|
||||
"""处理未捕获的异常"""
|
||||
trace_id_value = None
|
||||
try:
|
||||
trace_id_value = getattr(g, 'trace_id', None)
|
||||
except Exception:
|
||||
trace_id_value = None
|
||||
|
||||
try:
|
||||
from flask import current_app
|
||||
current_app.logger.exception("Unhandled exception trace_id=%s", trace_id_value or "unknown")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
error_payload = build_error_payload(
|
||||
code="INTERNAL_ERROR",
|
||||
message="服务器内部错误",
|
||||
err_type="UnhandledException",
|
||||
status=500,
|
||||
details=str(error),
|
||||
trace_id=trace_id_value
|
||||
)
|
||||
|
||||
if request.path.startswith('/api/') or request.is_json:
|
||||
return jsonify({'success': False, 'error': error_payload}), 500
|
||||
|
||||
return f"服务器内部错误 (trace_id={error_payload.get('trace_id')})", 500
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Trace ID 中间件
|
||||
|
||||
功能:
|
||||
- 为每个请求生成/透传 trace_id
|
||||
- 在响应中添加 X-Trace-Id 头
|
||||
- 将 legacy 字符串错误格式标准化为结构化错误
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
from flask import g, request
|
||||
|
||||
from outlook_web.errors import build_error_payload, generate_trace_id
|
||||
|
||||
|
||||
def ensure_trace_id():
|
||||
"""为每个请求生成/透传 trace_id,便于前后端统一追踪"""
|
||||
incoming = request.headers.get('X-Trace-Id') or request.headers.get('X-Request-Id')
|
||||
if incoming:
|
||||
incoming = incoming.strip()
|
||||
g.trace_id = incoming[:64]
|
||||
else:
|
||||
g.trace_id = generate_trace_id()
|
||||
|
||||
|
||||
def attach_trace_id_and_normalize_errors(response):
|
||||
"""统一写入 X-Trace-Id,并把 legacy 的字符串错误格式标准化为结构化错误"""
|
||||
trace_id_value = None
|
||||
try:
|
||||
trace_id_value = getattr(g, 'trace_id', None)
|
||||
if trace_id_value:
|
||||
response.headers.setdefault('X-Trace-Id', trace_id_value)
|
||||
except Exception:
|
||||
trace_id_value = None
|
||||
|
||||
try:
|
||||
if response.is_streamed:
|
||||
return response
|
||||
|
||||
content_type = response.headers.get('Content-Type', '') or ''
|
||||
if not content_type.startswith('application/json'):
|
||||
return response
|
||||
|
||||
data = response.get_json(silent=True)
|
||||
if not isinstance(data, dict):
|
||||
return response
|
||||
|
||||
if data.get('success') is not False:
|
||||
return response
|
||||
|
||||
# 统一补齐 trace_id/status 等字段
|
||||
if isinstance(data.get('error'), dict):
|
||||
error_obj = dict(data['error'])
|
||||
mutated = False
|
||||
if not error_obj.get('trace_id') and trace_id_value:
|
||||
error_obj['trace_id'] = trace_id_value
|
||||
mutated = True
|
||||
if not error_obj.get('status'):
|
||||
error_obj['status'] = response.status_code if response.status_code >= 400 else 400
|
||||
mutated = True
|
||||
if not error_obj.get('code'):
|
||||
error_obj['code'] = 'UNKNOWN_ERROR'
|
||||
mutated = True
|
||||
if not error_obj.get('message'):
|
||||
error_obj['message'] = '请求失败'
|
||||
mutated = True
|
||||
if mutated:
|
||||
new_data = dict(data)
|
||||
new_data['error'] = error_obj
|
||||
response.set_data(json.dumps(new_data, ensure_ascii=False))
|
||||
return response
|
||||
|
||||
# legacy:error 为字符串
|
||||
if isinstance(data.get('error'), str):
|
||||
legacy_message = data.get('error') or '请求失败'
|
||||
status_for_payload = response.status_code if response.status_code >= 400 else 400
|
||||
error_payload = build_error_payload(
|
||||
code='LEGACY_ERROR',
|
||||
message=legacy_message,
|
||||
err_type='LegacyError',
|
||||
status=status_for_payload,
|
||||
details='',
|
||||
trace_id=trace_id_value
|
||||
)
|
||||
new_data = dict(data)
|
||||
new_data['error'] = error_payload
|
||||
response.set_data(json.dumps(new_data, ensure_ascii=False))
|
||||
return response
|
||||
except Exception:
|
||||
return response
|
||||
|
||||
return response
|
||||
Reference in new issue
Block a user