File size: 3,279 Bytes
69fb140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c78660
69fb140
6c78660
 
 
 
 
 
 
 
69fb140
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
"""
全域異常處理中間件
統一處理所有未捕獲的異常,返回標準化錯誤回應
"""

import logging
import traceback
from typing import Callable

from fastapi import Request, Response
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware

from core.exceptions import BloomWareException, handle_exception
from core.logging import get_logger

logger = get_logger("middleware.exception_handler")


class ExceptionHandlerMiddleware(BaseHTTPMiddleware):
    """
    全域異常處理中間件

    功能:
    1. 捕獲所有未處理的異常
    2. 轉換為標準化 JSON 錯誤回應
    3. 記錄錯誤日誌(生產環境隱藏堆疊)
    """

    async def dispatch(self, request: Request, call_next: Callable) -> Response:
        try:
            response = await call_next(request)
            return response

        except BloomWareException as e:
            # 已知的業務異常
            logger.warning(f"業務異常: {e.code} - {e.message}")
            return e.to_response()

        except Exception as e:
            # 未知異常
            logger.error(f"未處理的異常: {type(e).__name__}: {e}")
            logger.debug(f"堆疊追蹤:\n{traceback.format_exc()}")

            # 返回標準化錯誤回應
            return JSONResponse(
                status_code=500,
                content={
                    "success": False,
                    "error": {
                        "code": "INTERNAL_ERROR",
                        "message": "內部伺服器錯誤",
                        "details": {}
                    }
                }
            )


class RequestLoggingMiddleware(BaseHTTPMiddleware):
    """
    請求日誌中間件

    功能:
    1. 記錄所有 API 請求
    2. 計算請求處理時間
    3. 記錄回應狀態碼
    """

    async def dispatch(self, request: Request, call_next: Callable) -> Response:
        import time

        start_time = time.time()
        method = request.method
        path = request.url.path

        # 跳過健康檢查和靜態資源的日誌
        skip_paths = ["/health", "/static/", "/favicon.ico"]
        should_log = not any(path.startswith(p) for p in skip_paths)

        try:
            response = await call_next(request)
            process_time = (time.time() - start_time) * 1000

            # 只記錄錯誤或慢請求(超過 1000ms)
            if should_log:
                if response.status_code >= 400:
                    logger.warning(
                        f"{method} {path} - {response.status_code} - {process_time:.2f}ms"
                    )
                elif process_time > 1000:
                    logger.warning(
                        f"{method} {path} - SLOW - {response.status_code} - {process_time:.2f}ms"
                    )

            # 添加處理時間到回應標頭
            response.headers["X-Process-Time"] = f"{process_time:.2f}ms"
            return response

        except Exception as e:
            process_time = (time.time() - start_time) * 1000
            if should_log:
                logger.error(
                    f"{method} {path} - ERROR - {process_time:.2f}ms - {e}"
                )
            raise