iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
Modern Web

《NestJS 絕地求生手冊》:我用一年血淚換來的實戰排雷筆記系列 第 26 篇

Day 26|消失的全域防線:為什麼加了局部 Filter 後,Global Exception Filter 就不再觸發?

  • 分享至 

  • xImage
  •  

團隊在 NestJS 中建立了 Global Exception Filter,希望統一處理 API 錯誤,包括格式化回應、隱藏內部錯誤訊息等。原本其他 Controller 都運作正常,但就在我們套用局部 Filter 後,一切都發生了改變。

全域防線看似失效,但真的是這樣嗎?今天就來帶大家拆解 NestJS Exception Filter 的執行機制,以及為什麼局部 Filter 會影響全域的錯誤處理。

問題怎麼發生?

為了重現這個情境,我們先註冊一個全域 Filter:

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter<unknown> {
  catch(exception: unknown, host: ArgumentsHost): void {
    const response = host.switchToHttp().getResponse<Response>();
    const isHttpException = exception instanceof HttpException;

    const status = isHttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    const message = isHttpException
      ? exception.message
      : 'Internal server error';

    response
      // 標記這是由 Global Filter 處理的
      .setHeader('x-error-filter', 'global')
      .status(status)
      .json({
        statusCode: status,
        message,
      });
  }
}

把它設定為全域:

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionFilter,
    },
  ],
})
export class Day26ControllerFilterSkipsGlobalModule {}

確認 Global Filter 正常運作

新增 PostsController 及測試端點:

@Controller('posts')
export class PostsController {
  @Get('missing')
  missing(): never {
    throw new NotFoundException('Post 404 not found');
  }
}

啟動專案並發送請求測試:

curl -i http://localhost:3000/posts/missing

得到回應:

HTTP/1.1 404 Not Found
x-error-filter: global
{
  "statusCode": 404,
  "message": "Post 404 not found"
}

可以看到 Global Filter 正常發揮作用,也保留了 NotFoundException 的 404 狀態碼。

加上 Controller Filter 後,全域標記消失了

接著,我們在 Controller 層加上 @UseFilters():

@Controller('posts')
@UseFilters(HttpExceptionFilter)
export class PostsController {
  @Get('missing')
  missing(): never {
    throw new NotFoundException('Post 404 not found');
  }
}

這個 Filter 只負責捕捉 HttpException:

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
  catch(exception: HttpException, host: ArgumentsHost): void {
    const response = host.switchToHttp().getResponse<Response>();
    const status = exception.getStatus();

    response
      .setHeader('x-error-filter', 'controller')
      .status(status)
      .json({
        statusCode: status,
        message: exception.message,
      });
  }
}

呼叫相同情境的端點:

curl -i http://localhost:3000/posts/missing

這次的回應變成了:

HTTP/1.1 404 Not Found
x-error-filter: controller
{
  "statusCode": 404,
  "message": "Post 404 not found"
}

雖然同一個端點仍然拋出相同的 NotFoundException,狀態碼也同樣是 404,但套用 Controller Filter 後,Global Filter 處理過的痕跡卻完全消失了。

這不只是回應格式不同的問題。如果 Global Filter 還負責錯誤追蹤或寫入稽核紀錄,這些處理邏輯也不會執行,可能導致部分錯誤缺少監控或追蹤紀錄。

根因:Exception Filter 並非逐層處理,只執行第一個符合者

這個問題的核心其實在於開發者對 Filter 執行機制的預期落差:Nest 不會執行所有符合條件的 Filter,而是只要找到第一個符合例外類型的 Filter,就會直接交給它處理並停止尋找。

當請求進入系統時,Guard 和 Pipe 會由外而內依序執行:

https://ithelp.ithome.com.tw/upload/images/20261010/20184306IA2IWKaWcV.png

而 Interceptor 更是標準的洋蔥模型:

https://ithelp.ithome.com.tw/upload/images/20261010/20184306SWNr33n4sQ.png

這很容易讓人產生誤會,以為 Exception Filter 也會先經過 Global 再進入 Controller;或者處理完 Controller Filter 後,還會把控制權還給 Global Filter。

但 Exception Filter 的運作邏輯截然不同。當例外發生時,Nest 會先按照以下順序排列候選的 Filter:

https://ithelp.ithome.com.tw/upload/images/20261010/20184306Me6bIepL5s.png

接著,它會依序檢查這些 Filter 宣告的 @Catch() 類型,找到第一個符合者就立刻把錯誤交給它處理,並終止尋找。

以剛剛的 /posts/missing 為例,整個處理過程如下:

https://ithelp.ithome.com.tw/upload/images/20261010/20184306vzyRcz6IU6.png

因為 NotFoundException 繼承自 HttpException,所以 Controller Filter 成功攔截了它。Nest 把例外交出去後就收工了,自然不會再把同一個例外傳給後面的 Global Filter。

常見誤解

誤解一:Global Filter 在該 Controller 已經徹底失效?

為了確認影響範圍,我們在該 Controller 新增一個拋出一般 Error 的端點:

@Get('unexpected')
unexpected(): never {
  throw new Error('Database connection unexpectedly failed');
}

發送請求:

curl -i http://localhost:3000/posts/unexpected

測試結果顯示會由 Global Filter 處理:

HTTP/1.1 500 Internal Server Error
x-error-filter: global
{
  "statusCode": 500,
  "message": "Internal server error"
}

會有這樣的結果,是因為 Controller Filter 明確宣告了 @Catch(HttpException),所以當遇到一般的 Error 時,這個局部 Filter 並不會攔截。Nest 判定條件不符後會繼續往外尋找,最後自然交由設定為 @Catch()(捕捉所有例外)的 Global Filter 順利接手。

這證明了局部 Filter 並不是「關掉」整個 Controller 的全域設定,它只會攔下自己宣告要處理的特定例外。

誤解二:重新 throw 就能把例外傳給下一個 Filter?

習慣了 Middleware 或 Interceptor 機制的開發者,遇到 Global Filter 沒被執行的情況,第一個直覺往往是:既然沒有 next() 可以呼叫,那我在局部 Filter 做完處理後,直接重新 throw exception,是不是就能交棒給外層的 Global Filter 了?

@Catch(HttpException)
export class HttpExceptionFilter {
  catch(exception: HttpException): void {
    console.log('controller filter handled');

    // 試圖拋出錯誤來交棒給下一個 Filter
    throw exception;
  }
}

千萬別這麼做!Exception Filter 既沒有 Express middleware 的 next(error),也沒有 Interceptor 的 next.handle() 交棒機制。

Nest 選出第一個符合的 Filter 後,該 Filter 就是執行的終點。如果在 catch() 裡再次拋出錯誤,不僅無法將控制權還給 Global Filter,反而會因為錯誤未被妥善處理,導致未預期的錯誤。

排雷指南:透過繼承重用共同的錯誤處理邏輯

如果我們希望局部 Filter 在處理特定需求的同時,仍然沿用共用的錯誤回應格式,NestJS 在請求生命週期文件中建議,可以透過繼承重用不同 Filter 的處理邏輯:

@Catch(HttpException)
export class HttpExceptionFilter extends GlobalExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost): void {
    console.log('[posts] controller-specific error context');

    // 呼叫父類別的共用邏輯
    super.catch(exception, host);
  }
}

此時的流程會變成這樣:

https://ithelp.ithome.com.tw/upload/images/20261010/201843060Y2le0nxuu.png

要注意的是,這不代表 NestJS 重新執行了 Global Filter。這次例外仍然只會由 Controller Filter 接手,super.catch() 只是透過繼承呼叫父類別的方法。

如果未來有更多 Filter 需要共用 Logger、Sentry 等功能,也可以考慮將這些功能抽離成服務,透過依賴注入重用,而不是全部集中在 Filter 的繼承關係中。

⚠️ 注意:
使用 @UseFilters() 時,建議傳入 Filter 類別,讓 NestJS 管理實例建立與依賴注入。如果 Filter 需要注入其他服務,應由 NestJS DI 容器管理,避免自行建立實例。

總結

  1. Filter 的尋找方向是由內而外:Nest 會依照 method-scoped → controller-scoped → global-scoped 的順序,從最接近路由的層級開始往外檢查。
  2. 只執行第一個符合的 Filter:Controller Filter 一旦捕捉到例外(如 HttpException),同一個例外就不會再進入 Global Filter,沒有自動交棒機制。
  3. Global Filter 並未整體失效:如果發生的例外不符合局部 Filter 的 @Catch() 條件(例如一般的 Error),Nest 依然會繼續往外找,最後由 Global Filter 處理。
  4. 需要延續共用行為時,請善用繼承:讓局部 Filter 繼承全域 Filter,並在處理完局部邏輯後透過 super.catch() 顯式重用全域的處理流程。
  5. 重新 throw 不是交棒:Exception Filter 沒有 next() 機制,從 catch() 中再次拋出例外並不會讓 Nest 接著執行下一個 Filter。

參考資料


上一篇
Day 25|失效的防護:明明加了 @Exclude(),為什麼 API 還是洩漏敏感資料?
系列文
《NestJS 絕地求生手冊》:我用一年血淚換來的實戰排雷筆記 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言