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:
胡绍康andClaude Opus 4.6 committed 2026-02-25 11:47:45 +08:00
1 parent f31f042d89
commit c0562ef721
4 files changed
+200 -5

No files matched your search

+11 -5
View File
@@ -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))
+16
View File
@@ -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",
]
+78
View File
@@ -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
+95
View File
@@ -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