ASP.NET Core APIにおけるグローバル例外処理ミドルウェアの実装とパイプライン適用

ASP.NET Core アプリケーションにおいて、外部クライアントに提供する API は一律のレスポンス形式を維持することが望ましい。予期せぬ例外が発生した場合、デフォルトの HTTP エラーページ(404 や 500 など)が返されると、クライアント側の JSON パーサーが破綻する可能性がある。そのため、リクエストパイプライン全体を巻き包み、例外発生時に構造化された JSON データを返却するミドルウェアを自作する手法が一般的である。

ターゲットとするレスポンス構造

ビジネスロジック層やインフラストラクチャ層で発生したエラーを、次のような一意の JSON フォーマットで統一する。

{
  "statusCode": 500,
  "message": "リソースの処理中にエラーが発生しました",
  "detail": "/api/v1/users/auth"
}

ミドルウェアの実装

リクエストパイプラインの各ステップを例外ハンドリングブロックで保護し、エラー発生時には HTTP ステータスコードとコンテンツタイプを適切に設定してシリアライズした文字列を返却する。ここでは標準ライブラリを採用し、不要な XML 変換処理を排除して実装を簡素化・モダン化する。

using System;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace ApiCore.Middleware
{
    public class GlobalErrorHandlingMiddleware
    {
        private readonly RequestDelegate _pipelineNext;

        public GlobalErrorHandlingMiddleware(RequestDelegate pipelineNext)
        {
            _pipelineNext = pipelineNext;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            try
            {
                await _pipelineNext(context);
            }
            catch (Exception exception)
            {
                await FormatErrorResponseAsync(context, exception);
            }
        }

        private static async Task FormatErrorResponseAsync(HttpContext context, Exception exception)
        {
            // カスタム業務例外かシステム例外かでステータスコードを分岐
            var httpStatusCode = exception is DomainBusinessException customEx
                ? customEx.HttpStatusCode
                : StatusCodes.Status500InternalServerError;

            context.Response.StatusCode = httpStatusCode;
            context.Response.ContentType = "application/json; charset=utf-8";

            var errorPayload = new UnifiedApiError
            {
                StatusCode = httpStatusCode,
                Message = exception.Message,
                Detail = context.Request.Path.Value ?? string.Empty
            };

            var jsonSettings = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
            var serializedJson = JsonSerializer.Serialize(errorPayload, jsonSettings);
            await context.Response.WriteAsync(serializedJson);
        }
    }

    /// <summary>
    /// 業務ロジック固有の例外ベースクラス
    /// </summary>
    public class DomainBusinessException : Exception
    {
        public int HttpStatusCode { get; }
        public DomainBusinessException(string message, int statusCode) : base(message)
        {
            HttpStatusCode = statusCode;
        }
    }

    /// <summary>
    /// API 共通エラーレスポンスモデル
    /// </summary>
    public class UnifiedApiError
    {
        public int StatusCode { get; set; }
        public string Message { get; set; }
        public string Detail { get; set; }
    }
}

アプリケーションパイプラインへの登録

作成したミドルウェアは、設定ファイルの初期化メソッド内でルーティングおよびコントローラーのマッピング処理より前に配置する必要がある。これにより、コンポーネント層でスローされたあらゆる例外を確実に捕捉できるようになる。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // 本番環境では詳細なエラーページを無効化し、ミドルウェアに委譲
        app.UseHsts();
    }

    // グローバル例外処理ミドルウェアの注入(順序は重要)
    app.UseMiddleware<GlobalErrorHandlingMiddleware>();

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

この構成により、コントローラーやサービス層でカスタム例外を明示的にスローした場合でも、ミドルウェアがインターセプトし、規定の JSON 構造体をクライアントへ返却する。デフォルトのフレームワーク例外と区別するため、独自のエラータイプを定義してステータスコードやエラーカテゴリを制御すると、運用フェーズでのデバッグやログ監視が格段に効率化する。

タグ: ASP.NET Core Middleware Exception Handling RESTful API C#

5月26日 11:39 投稿