ASP.NET Core Web API のエラーを処理する

この記事では、ASP.NET Core Web API を使用したエラー処理とエラー処理のカスタマイズ方法について説明します。

開発者例外ページ

開発者例外ページには、サーバー エラーの詳しいスタック トレースが示されています。 DeveloperExceptionPageMiddleware を使用して、HTTP パイプラインから同期および非同期例外をキャプチャし、エラー応答を生成します。 たとえば、例外をスローする次のコントローラー アクションを考えてみましょう:

[HttpGet("Throw")]
public IActionResult Throw() =>
    throw new Exception("Sample exception.");

開発者例外ページで未処理の例外が検出されると、次の例のような既定のプレーンテキスト応答が生成されます:

HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

System.Exception: Sample exception.
   at HandleErrorsSample.Controllers.ErrorsController.Get() in ...
   at lambda_method1(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()

...

クライアントが HTML 形式の応答を要求すると、開発者例外ページでは次の例のような応答が生成されます:

HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="utf-8" />
        <title>Internal Server Error</title>
        <style>
            body {
    font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif;
    font-size: .813em;
    color: #222;
    background-color: #fff;
}

h1 {
    color: #44525e;
    margin: 15px 0 15px 0;
}

...

HTML 形式の応答を要求するには、Accept HTTP 要求ヘッダを text/html に設定します。

警告

アプリを開発環境で実行しない限り、開発者例外ページを有効にしないでください。 アプリを運用環境で実行するときは、詳細な例外情報を公開しないでください。 環境の構成について詳しくは、「ASP.NET Core で複数の環境を使用する」を参照してください。

例外ハンドラー

開発以外の環境では、例外処理ミドルウェアを使用してエラー ペイロードを生成します:

  1. Program.csUseExceptionHandler を呼び出して、例外処理ミドルウェアを追加します:

    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error");
    }
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    
  2. /error ルートに応答するようにコントローラー アクションを構成します。

    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

前の HandleError アクションは、RFC 7807 準拠のペイロードをクライアントに送信します。

警告

HttpGet などの HTTP メソッド属性を使ってエラー ハンドラー アクション メソッドをマークしないでください。 明示的な動詞を使用すると、要求がアクション メソッドに届かないことがあります。

Swagger/OpenAPI を使用する Web API の場合は、エラー ハンドラー アクションを [ApiExplorerSettings] 属性でマークし、その IgnoreApi プロパティを true に設定します。 この属性の構成では、アプリの OpenAPI 仕様からエラー ハンドラー アクションが除外されます:

[ApiExplorerSettings(IgnoreApi = true)]

認証されていないユーザーがエラーを見る必要がある場合は、メソッドへの匿名アクセスを許可します。

例外処理ミドルウェアを開発環境で使用して、すべての環境で一貫したペイロード形式を生成することもできます:

  1. Program.cs で、環境固有の例外処理ミドルウェア インスタンスを登録します。

    if (app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error-development");
    }
    else
    {
        app.UseExceptionHandler("/error");
    }
    

    前のコードでは、ミドルウェアは次のように登録されます。

    • 開発環境では /error-development のルート。
    • 開発環境以外の /error のルート。

  2. 開発ルートと開発以外のルートの両方にコントローラー アクションを追加します:

    [Route("/error-development")]
    public IActionResult HandleErrorDevelopment(
        [FromServices] IHostEnvironment hostEnvironment)
    {
        if (!hostEnvironment.IsDevelopment())
        {
            return NotFound();
        }
    
        var exceptionHandlerFeature =
            HttpContext.Features.Get<IExceptionHandlerFeature>()!;
    
        return Problem(
            detail: exceptionHandlerFeature.Error.StackTrace,
            title: exceptionHandlerFeature.Error.Message);
    }
    
    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

例外を使用して応答を変更する

応答の内容は、カスタム例外とアクション フィルターを使用して、コントローラーの外部から変更できます:

  1. HttpResponseException という名前の一般的な例外の種類を作成します。

    public class HttpResponseException : Exception
    {
        public HttpResponseException(int statusCode, object? value = null) =>
            (StatusCode, Value) = (statusCode, value);
    
        public int StatusCode { get; }
    
        public object? Value { get; }
    }
    
  2. HttpResponseExceptionFilter という名前のアクション フィルターを作成します。

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order => int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException httpResponseException)
            {
                context.Result = new ObjectResult(httpResponseException.Value)
                {
                    StatusCode = httpResponseException.StatusCode
                };
    
                context.ExceptionHandled = true;
            }
        }
    }
    

    上のフィルターでは、最大の整数値から 10 を引いた値が Order に指定されています。 この Order により、パイプラインの最後で他のフィルターを実行できます。

  3. Program.cs に、フィルター コレクションに対するアクション フィルターを追加します。

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add<HttpResponseExceptionFilter>();
    });
    

検証失敗のエラー応答

Web API コントローラーでは、モデルの検証が失敗すると、MVC が ValidationProblemDetails という応答の種類で応答します。 MVC は InvalidModelStateResponseFactory の結果を使用して、検証失敗に対するエラー応答を作成します。 次の例では、既定のファクトリを、Program.cs の XML としての書式設定応答もサポートする実装に置き換えます:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
            new BadRequestObjectResult(context.ModelState)
            {
                ContentTypes =
                {
                    // using static System.Net.Mime.MediaTypeNames;
                    Application.Json,
                    Application.Xml
                }
            };
    })
    .AddXmlSerializerFormatters();

クライアントのエラー応答

"エラー結果" は、HTTP 状態コードが 400 以上の結果として定義されます。 Web API コントローラーの場合、MVC によってエラー結果が変換され ProblemDetails を生成します。

エラー状態コードの ProblemDetails の自動作成は既定で有効になっていますが、エラー応答は次のいずれかの方法で構成できます:

  1. 問題の詳細サービスを使用する
  2. ProblemDetailsFactory の実装
  3. ApiBehaviorOptions.ClientErrorMapping の使用

既定の問題の詳細の応答

次の Program.cs ファイルは、API コントローラー用の Web アプリケーション テンプレートによって生成されました:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

入力が無効な場合に BadRequest を返す次のコントローラーについて考えてみましょう:

[Route("api/[controller]/[action]")]
[ApiController]
public class Values2Controller : ControllerBase
{
    // /api/values2/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values2 /squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }
}

次のいずれかの条件が適用されると、前のコードで問題の詳細の応答が生成されます:

  • /api/values2/divide エンドポイントは、0 分母で呼び出されます。
  • /api/values2/squareroot エンドポイントは、0 未満の被開平数で呼び出されます。

既定の問題の詳細応答本文には、次の typetitle、および status の値があります:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "Bad Request",
  "status": 400,
  "traceId": "00-84c1fd4063c38d9f3900d06e56542d48-85d1d4-00"
}

問題の詳細サービス

ASP.NET Core では、IProblemDetailsService を使用した HTTP API の問題の詳細の作成がサポートされています。 詳細については、「問題の詳細サービス」を参照してください。

次のコードは、"本文コンテンツがまだ含まれていない" すべての HTTP クライアントおよびサーバー エラー応答に対して問題の詳細の応答を生成するようにアプリを構成します。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();

入力が無効な場合に BadRequest を返す、前のセクションの API コントローラーについて考えてみましょう:

[Route("api/[controller]/[action]")]
[ApiController]
public class Values2Controller : ControllerBase
{
    // /api/values2/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values2 /squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }
}

次のいずれかの条件が適用されると、前のコードで問題の詳細の応答が生成されます:

  • 無効な入力が指定されています。
  • URI に一致するエンドポイントがありません。
  • ハンドルされない例外が発生しました。

SuppressMapClientErrors プロパティが true に設定されている場合、エラー状態コード用の ProblemDetails の自動作成は無効になります。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressMapClientErrors = true;
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

上記のコードを使用して、API コントローラーが BadRequest を返すと、応答本文なしで HTTP 400 応答状態が返されます。 SuppressMapClientErrors は、API コントローラー エンドポイント用に WriteAsync を呼び出す場合でも、ProblemDetails 応答が作成されないようにします。 WriteAsync については、この記事で後ほど説明します。

次のセクションでは、CustomizeProblemDetails を使用して問題の詳細の応答本文をカスタマイズして、より役に立つ応答を返す方法を示します。 その他のカスタマイズ オプションについては、「問題の詳細のカスタマイズ」を参照してください。

CustomizeProblemDetails を使って、問題の詳細をカスタマイズする

次のコードは ProblemDetailsOptions を使用して CustomizeProblemDetails を設定します。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails(options =>
        options.CustomizeProblemDetails = (context) =>
        {

            var mathErrorFeature = context.HttpContext.Features
                                                       .Get<MathErrorFeature>();
            if (mathErrorFeature is not null)
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError =>
                    ("Divison by zero is not defined.",
                                          "https://wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                                          "https://wikipedia.org/wiki/Square_root")
                };

                context.ProblemDetails.Type = details.Type;
                context.ProblemDetails.Title = "Bad Input";
                context.ProblemDetails.Detail = details.Detail;
            }
        }
    );

var app = builder.Build();

app.UseHttpsRedirection();

app.UseStatusCodePages();

app.UseAuthorization();

app.MapControllers();

app.Run();

更新された API コントローラー:

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}

次のコードには、前のサンプルで使用された MathErrorFeatureMathErrorTypeが含まれています:

// Custom Http Request Feature
class MathErrorFeature
{
    public MathErrorType MathError { get; set; }
}

// Custom math errors
enum MathErrorType
{
    DivisionByZeroError,
    NegativeRadicandError
}

次のいずれかの条件が適用されると、前のコードで問題の詳細の応答が生成されます:

  • /divide エンドポイントは、0 分母で呼び出されます。
  • /squareroot エンドポイントは 0 未満の被開平数で呼び出されます。
  • URI に一致するエンドポイントがありません。

いずれかの squareroot エンドポイントが 0 未満の被開平数で呼び出された場合、問題の詳細応答本文には次のものが含まれます:

{
  "type": "https://en.wikipedia.org/wiki/Square_root",
  "title": "Bad Input",
  "status": 400,
  "detail": "Negative or complex numbers are not allowed."
}

サンプル コードを表示またはダウンロードする

ProblemDetailsFactory を実装する

MVC は、Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory を使用して、ProblemDetailsValidationProblemDetails のすべてのインスタンスを生成します。 このファクトリは、次の目的で使用されます:

問題の詳しい応答をカスタマイズするには、Program.csProblemDetailsFactory のカスタム実装を登録します。

builder.Services.AddControllers();
builder.Services.AddTransient<ProblemDetailsFactory, SampleProblemDetailsFactory>();

ApiBehaviorOptions.ClientErrorMapping を使用します

ProblemDetails の応答の内容を構成するには、ClientErrorMapping プロパティを使用します。 たとえば、Program.csの次のコードにより、404 応答の Link プロパティが更新されます。

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

その他のリソース

この記事では、ASP.NET Core Web API を使用したエラー処理とエラー処理のカスタマイズ方法について説明します。

開発者例外ページ

開発者例外ページには、サーバー エラーの詳しいスタック トレースが示されています。 DeveloperExceptionPageMiddleware を使用して、HTTP パイプラインから同期および非同期例外をキャプチャし、エラー応答を生成します。 たとえば、例外をスローする次のコントローラー アクションを考えてみましょう:

[HttpGet("Throw")]
public IActionResult Throw() =>
    throw new Exception("Sample exception.");

開発者例外ページで未処理の例外が検出されると、次の例のような既定のプレーンテキスト応答が生成されます:

HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

System.Exception: Sample exception.
   at HandleErrorsSample.Controllers.ErrorsController.Get() in ...
   at lambda_method1(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()

...

クライアントが HTML 形式の応答を要求すると、開発者例外ページでは次の例のような応答が生成されます:

HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="utf-8" />
        <title>Internal Server Error</title>
        <style>
            body {
    font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif;
    font-size: .813em;
    color: #222;
    background-color: #fff;
}

h1 {
    color: #44525e;
    margin: 15px 0 15px 0;
}

...

HTML 形式の応答を要求するには、Accept HTTP 要求ヘッダを text/html に設定します。

警告

アプリを開発環境で実行しない限り、開発者例外ページを有効にしないでください。 アプリを運用環境で実行するときは、詳細な例外情報を公開しないでください。 環境の構成について詳しくは、「ASP.NET Core で複数の環境を使用する」を参照してください。

例外ハンドラー

開発以外の環境では、例外処理ミドルウェアを使用してエラー ペイロードを生成します:

  1. Program.csUseExceptionHandler を呼び出して、例外処理ミドルウェアを追加します:

    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error");
    }
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    
  2. /error ルートに応答するようにコントローラー アクションを構成します。

    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

前の HandleError アクションは、RFC 7807 準拠のペイロードをクライアントに送信します。

警告

HttpGet などの HTTP メソッド属性を使ってエラー ハンドラー アクション メソッドをマークしないでください。 明示的な動詞を使用すると、要求がアクション メソッドに届かないことがあります。

Swagger/OpenAPI を使用する Web API の場合は、エラー ハンドラー アクションを [ApiExplorerSettings] 属性でマークし、その IgnoreApi プロパティを true に設定します。 この属性の構成では、アプリの OpenAPI 仕様からエラー ハンドラー アクションが除外されます:

[ApiExplorerSettings(IgnoreApi = true)]

認証されていないユーザーがエラーを見る必要がある場合は、メソッドへの匿名アクセスを許可します。

例外処理ミドルウェアを開発環境で使用して、すべての環境で一貫したペイロード形式を生成することもできます:

  1. Program.cs で、環境固有の例外処理ミドルウェア インスタンスを登録します。

    if (app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error-development");
    }
    else
    {
        app.UseExceptionHandler("/error");
    }
    

    前のコードでは、ミドルウェアは次のように登録されます。

    • 開発環境では /error-development のルート。
    • 開発環境以外の /error のルート。

  2. 開発ルートと開発以外のルートの両方にコントローラー アクションを追加します:

    [Route("/error-development")]
    public IActionResult HandleErrorDevelopment(
        [FromServices] IHostEnvironment hostEnvironment)
    {
        if (!hostEnvironment.IsDevelopment())
        {
            return NotFound();
        }
    
        var exceptionHandlerFeature =
            HttpContext.Features.Get<IExceptionHandlerFeature>()!;
    
        return Problem(
            detail: exceptionHandlerFeature.Error.StackTrace,
            title: exceptionHandlerFeature.Error.Message);
    }
    
    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

例外を使用して応答を変更する

応答の内容は、カスタム例外とアクション フィルターを使用して、コントローラーの外部から変更できます:

  1. HttpResponseException という名前の一般的な例外の種類を作成します。

    public class HttpResponseException : Exception
    {
        public HttpResponseException(int statusCode, object? value = null) =>
            (StatusCode, Value) = (statusCode, value);
    
        public int StatusCode { get; }
    
        public object? Value { get; }
    }
    
  2. HttpResponseExceptionFilter という名前のアクション フィルターを作成します。

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order => int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException httpResponseException)
            {
                context.Result = new ObjectResult(httpResponseException.Value)
                {
                    StatusCode = httpResponseException.StatusCode
                };
    
                context.ExceptionHandled = true;
            }
        }
    }
    

    上のフィルターでは、最大の整数値から 10 を引いた値が Order に指定されています。 この Order により、パイプラインの最後で他のフィルターを実行できます。

  3. Program.cs に、フィルター コレクションに対するアクション フィルターを追加します。

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add<HttpResponseExceptionFilter>();
    });
    

検証失敗のエラー応答

Web API コントローラーでは、モデルの検証が失敗すると、MVC が ValidationProblemDetails という応答の種類で応答します。 MVC は InvalidModelStateResponseFactory の結果を使用して、検証失敗に対するエラー応答を作成します。 次の例では、既定のファクトリを、Program.cs の XML としての書式設定応答もサポートする実装に置き換えます:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
            new BadRequestObjectResult(context.ModelState)
            {
                ContentTypes =
                {
                    // using static System.Net.Mime.MediaTypeNames;
                    Application.Json,
                    Application.Xml
                }
            };
    })
    .AddXmlSerializerFormatters();

クライアントのエラー応答

"エラー結果" は、HTTP 状態コードが 400 以上の結果として定義されます。 Web API コントローラーの場合、MVC によってエラー結果が変換され ProblemDetails を生成します。

エラー応答は、次のいずれかの方法で構成できます。

  1. ProblemDetailsFactory の実装
  2. ApiBehaviorOptions.ClientErrorMapping の使用

ProblemDetailsFactory を実装する

MVC は、Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory を使用して、ProblemDetailsValidationProblemDetails のすべてのインスタンスを生成します。 このファクトリは、次の目的で使用されます:

問題の詳しい応答をカスタマイズするには、Program.csProblemDetailsFactory のカスタム実装を登録します。

builder.Services.AddControllers();
builder.Services.AddTransient<ProblemDetailsFactory, SampleProblemDetailsFactory>();

ApiBehaviorOptions.ClientErrorMapping を使用します

ProblemDetails の応答の内容を構成するには、ClientErrorMapping プロパティを使用します。 たとえば、Program.csの次のコードにより、404 応答の Link プロパティが更新されます。

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

例外を処理するカスタム ミドルウェア

例外処理ミドルウェアの既定値は、ほとんどのアプリで問題なく機能します。 特殊な例外処理が必要なアプリの場合は、例外処理ミドルウェアのカスタマイズを検討してください。

例外のための ProblemDetails ペイロードの生成

サーバーでハンドルされない例外が発生したとき、ASP.NET Core では標準化されたエラー ペイロードは生成されません。 標準化された ProblemDetails 応答をクライアントに返すのが望ましいシナリオでは、ProblemDetails ミドルウェアを使って、例外と 404 応答を ProblemDetails ペイロードにマップできます。 例外処理ミドルウェアを使って、ハンドルされない例外の ProblemDetails ペイロードを返すこともできます。

その他のリソース

この記事では、ASP.NET Core Web API を使用したエラーの処理方法とエラー処理のカスタマイズ方法について説明します。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

開発者例外ページ

開発者例外ページは、サーバー エラーの詳しいスタック トレースを取得するために役立つツールです。 DeveloperExceptionPageMiddleware を使用して、HTTP パイプラインから同期および非同期例外をキャプチャし、エラー応答を生成します。 これを説明するために、次のコントローラー アクションがあるとします。

[HttpGet("{city}")]
public WeatherForecast Get(string city)
{
    if (!string.Equals(city?.TrimEnd(), "Redmond", StringComparison.OrdinalIgnoreCase))
    {
        throw new ArgumentException(
            $"We don't offer a weather forecast for {city}.", nameof(city));
    }
    
    return GetWeather().First();
}

次の curl コマンドを実行して、上記のアクションをテストします。

curl -i https://localhost:5001/weatherforecast/chicago

クライアントが HTML 形式の出力を要求しない場合、開発者例外ページにはテキスト形式の応答が表示されます。 次のような出力が表示されます。

HTTP/1.1 500 Internal Server Error
Transfer-Encoding: chunked
Content-Type: text/plain
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Date: Fri, 27 Sep 2019 16:13:16 GMT

System.ArgumentException: We don't offer a weather forecast for chicago. (Parameter 'city')
   at WebApiSample.Controllers.WeatherForecastController.Get(String city) in C:\working_folder\aspnet\AspNetCore.Docs\aspnetcore\web-api\handle-errors\samples\3.x\Controllers\WeatherForecastController.cs:line 34
   at lambda_method(Closure , Object , Object[] )
   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS
=======
Accept: */*
Host: localhost:44312
User-Agent: curl/7.55.1

代わりに HTML 形式の応答を表示するには、Accept HTTP 要求ヘッダーを text/html のメディアの種類に設定します。 次に例を示します。

curl -i -H "Accept: text/html" https://localhost:5001/weatherforecast/chicago

HTTP 応答からの次の抜粋を考えてみます。

HTTP/1.1 500 Internal Server Error
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Date: Fri, 27 Sep 2019 16:55:37 GMT

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="utf-8" />
        <title>Internal Server Error</title>
        <style>
            body {
    font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif;
    font-size: .813em;
    color: #222;
    background-color: #fff;
}

HTML 形式の応答は、Postman などのツールを使用してテストするときに役立ちます。 次の画面キャプチャは、Postman のテキスト形式と HTML 形式の両方の応答を示しています。

Test the Developer Exception Page in Postman.

警告

アプリを開発環境で実行するときにのみ、開発者例外ページを有効にします。 アプリを運用環境で実行するときは、詳細な例外情報を公開しないでください。 環境の構成について詳しくは、「ASP.NET Core で複数の環境を使用する」を参照してください。

HttpGet などの HTTP メソッド属性を使ってエラー ハンドラー アクション メソッドをマークしないでください。 明示的な動詞を使用すると、要求がアクション メソッドに届かないことがあります。 認証されていないユーザーがエラーを見る必要がある場合は、メソッドへの匿名アクセスを許可します。

例外ハンドラー

開発以外の環境では、例外処理ミドルウェアを使用してエラー ペイロードを生成できます。

  1. Startup.Configure で、UseExceptionHandler を呼び出してミドルウェアを使用します。

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/error");
        }
    
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  2. /error ルートに応答するようにコントローラー アクションを構成します。

    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error() => Problem();
    }
    

前の Error アクションは、RFC 7807 準拠のペイロードをクライアントに送信します。

ローカル開発環境では、例外処理ミドルウェアによって、さらに詳しいコンテンツ ネゴシエーション結果も提供されます。 次の手順に従い、開発環境と運用環境で一貫性のあるペイロード形式を生成します。

  1. Startup.Configure で、環境固有の例外処理ミドルウェア インスタンスを登録します。

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseExceptionHandler("/error-local-development");
        }
        else
        {
            app.UseExceptionHandler("/error");
        }
    }
    

    前のコードでは、ミドルウェアは次のように登録されます。

    • 開発環境では /error-local-development のルート。
    • 開発以外の環境では /error のルート。

  2. 属性ルーティングをコントローラー アクションに適用します。

    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error-local-development")]
        public IActionResult ErrorLocalDevelopment(
            [FromServices] IWebHostEnvironment webHostEnvironment)
        {
            if (webHostEnvironment.EnvironmentName != "Development")
            {
                throw new InvalidOperationException(
                    "This shouldn't be invoked in non-development environments.");
            }
    
            var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
    
            return Problem(
                detail: context.Error.StackTrace,
                title: context.Error.Message);
        }
    
        [Route("/error")]
        public IActionResult Error() => Problem();
    }
    

    前のコードでは、ControllerBase.Problem を呼び出して ProblemDetails 応答が作成されています。

例外を使用して応答を変更する

応答の内容は、コントローラーの外部で変更できます。 ASP.NET 4.x Web API の場合、これを行う方法の 1 つが HttpResponseException 型の使用でした。 ASP.NET Core には同等の型が含まれていません。 HttpResponseException のサポートは以下の手順で追加することができます。

  1. HttpResponseException という名前の一般的な例外の種類を作成します。

    public class HttpResponseException : Exception
    {
        public int Status { get; set; } = 500;
    
        public object Value { get; set; }
    }
    
  2. HttpResponseExceptionFilter という名前のアクション フィルターを作成します。

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order { get; } = int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException exception)
            {
                context.Result = new ObjectResult(exception.Value)
                {
                    StatusCode = exception.Status,
                };
                context.ExceptionHandled = true;
            }
        }
    }
    

    上のフィルターでは、最大の整数値から 10 を引いた値が Order に指定されています。 この Order により、パイプラインの最後で他のフィルターを実行できます。

  3. Startup.ConfigureServices に、フィルター コレクションに対するアクション フィルターを追加します。

    services.AddControllers(options =>
        options.Filters.Add(new HttpResponseExceptionFilter()));
    

検証失敗のエラー応答

Web API コントローラーでは、モデルの検証が失敗すると、MVC が ValidationProblemDetails という応答の種類で応答します。 MVC は InvalidModelStateResponseFactory の結果を使用して、検証失敗に対するエラー応答を作成します。 次の例では、Startup.ConfigureServices で、ファクトリを使用して応答の既定の種類を SerializableError に変更します。

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var result = new BadRequestObjectResult(context.ModelState);

            // TODO: add `using System.Net.Mime;` to resolve MediaTypeNames
            result.ContentTypes.Add(MediaTypeNames.Application.Json);
            result.ContentTypes.Add(MediaTypeNames.Application.Xml);

            return result;
        };
    });

クライアントのエラー応答

"エラー結果" は、HTTP 状態コードが 400 以上の結果として定義されます。 Web API コントローラーの場合、MVC によってエラー結果が ProblemDetails を含む結果に変換されます。

エラー応答は、次のいずれかの方法で構成できます。

  1. ProblemDetailsFactory の実装
  2. ApiBehaviorOptions.ClientErrorMapping の使用

ProblemDetailsFactory を実装する

MVC は、Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory を使用して、ProblemDetailsValidationProblemDetails のすべてのインスタンスを生成します。 このファクトリは、次の目的で使用されます:

問題の詳しい応答をカスタマイズするには、Startup.ConfigureServicesProblemDetailsFactory のカスタム実装を登録します。

public void ConfigureServices(IServiceCollection serviceCollection)
{
    services.AddControllers();
    services.AddTransient<ProblemDetailsFactory, CustomProblemDetailsFactory>();
}

ApiBehaviorOptions.ClientErrorMapping の使用

ProblemDetails の応答の内容を構成するには、ClientErrorMapping プロパティを使用します。 たとえば、Startup.ConfigureServicesの次のコードにより、404 応答の type プロパティが更新されます。

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
        options.DisableImplicitFromServicesParameters = true;
    });

例外を処理するカスタム ミドルウェア

例外処理ミドルウェアの既定値は、ほとんどのアプリで問題なく機能します。 特殊な例外処理が必要なアプリの場合は、例外処理ミドルウェアのカスタマイズを検討してください。

例外のための ProblemDetails ペイロードの生成

サーバーでハンドルされない例外が発生したとき、ASP.NET Core では標準化されたエラー ペイロードは生成されません。 標準化された ProblemDetails 応答をクライアントに返すのが望ましいシナリオでは、ProblemDetails ミドルウェアを使って、例外と 404 応答を ProblemDetails ペイロードにマップできます。 例外処理ミドルウェアを使って、ハンドルされない例外の ProblemDetails ペイロードを返すこともできます。