次の方法で共有


ASP.NET Core コントローラーベースの Web API でエラーを処理する

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

この記事では、コントローラーベースの ASP.NET Core Web API でエラーを処理し、エラー処理をカスタマイズする方法について説明します。 Minimal API でのエラー処理の詳細については、「ASP.NET Core のエラーを処理する」と Minimal API アプリでエラーを処理する方法に関する記事を参照してください。

開発者例外ページ

"開発者例外ページ" には、未処理の要求の例外に関する詳細な情報が表示されます。 DeveloperExceptionPageMiddleware を使用して、HTTP パイプラインから同期および非同期例外をキャプチャし、エラー応答を生成します。 開発者例外ページは、後続のミドルウェアでスローされた未処理の例外をキャッチできるように、ミドルウェア パイプラインの早い段階で実行されます。

次の両方が当てはまる場合、ASP.NET Core アプリでは既定で開発者例外ページが有効になります。

以前のテンプレート、つまり WebHost.CreateDefaultBuilder を使用して作成されたアプリは、app.UseDeveloperExceptionPage を呼び出すことによって開発者例外ページを有効にすることができます。

警告

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

開発者例外ページには、例外と要求に関する次の情報が含まれている場合があります。

  • スタック トレース
  • クエリ文字列のパラメーター (ある場合)
  • Cookie (ある場合)
  • ヘッダー
  • エンドポイント メタデータ (存在する場合)

開発者例外ページで何らかの情報が提供されるとは限りません。 完全なエラー情報については、ログ記録に関するページを参照してください。

次の図は、タブと表示される情報を示すアニメーション付きのサンプルの開発者例外ページを示しています。

選択された各タブを表示するようにアニメーション化された開発者例外ページ。

Accept: text/plain ヘッダーを含む要求への応答で、開発者例外ページは HTML ではなくプレーン テキストを返します。 次に例を示します。

Status: 500 Internal Server Error
Time: 9.39 msSize: 480 bytes
FormattedRawHeadersRequest
Body
text/plain; charset=utf-8, 480 bytes
System.InvalidOperationException: Sample Exception
   at WebApplicationMinimal.Program.<>c.<Main>b__0_0() in C:\Source\WebApplicationMinimal\Program.cs:line 12
   at lambda_method1(Closure, Object, HttpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: text/plain
Host: localhost:7267
traceparent: 00-0eab195ea19d07b90a46cd7d6bf2f

開発者例外ページを確認するには:

  • コントローラーベースの API に次のコントローラー アクションを追加します。 エンドポイントが要求されると、このアクションによって例外がスローされます。

    [HttpGet("Throw")]
    public IActionResult Throw() =>
        throw new Exception("Sample exception.");
    
  • 開発環境でアプリを実行します。

  • コントローラー アクションで定義されたエンドポイントに移動します。

例外ハンドラー

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

  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 形式の応答は、curl などのツールを使用してテストするときに役立ちます。

警告

アプリを開発環境で実行するときにのみ、開発者例外ページを有効にします。 アプリを運用環境で実行するときは、詳細な例外情報を公開しないでください。 環境の構成について詳しくは、「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 ペイロードを返すこともできます。