ASP.NET Core Web APIの例外処理


280

長年にわたって通常のASP.NET Web APIを使用した後、新しいREST APIプロジェクトにASP.NET Coreを使用しています。ASP.NET Core Web APIで例外を処理するための良い方法はありません。例外処理フィルター/属性を実装しようとしました:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

そして、これが私のスタートアップフィルター登録です:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

私が抱えていた問題は、例外が発生したときAuthorizationFilterにで処理されないことErrorHandlingFilterです。私はそれが古いASP.NET Web APIで動作するのと同じように、そこで見つかると思っていました。

それでは、すべてのアプリケーション例外とアクションフィルターからの例外をどのようにしてキャッチできますか?


3
UseExceptionHandlerミドルウェアを試しましたか?
Pawel

ミドルウェアの使用方法の例をここに示しますUseExceptionHandler
Ilya Chernomordik

回答:


539

例外処理ミドルウェア

さまざまな例外処理アプローチを使用して多くの実験を行った後、最終的にミドルウェアを使用しました。ASP.NET Core Web APIアプリケーションに最適です。アプリケーションの例外とアクションフィルターの例外を処理し、例外処理とHTTP応答を完全に制御できます。ここに私の例外処理ミドルウェアがあります:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if      (ex is MyNotFoundException)     code = HttpStatusCode.NotFound;
        else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
        else if (ex is MyException)             code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new { error = ex.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}

それを登録MVC前Startupクラス:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();

スタックトレース、例外タイプ名、エラーコードなど、必要なものを追加できます。非常に柔軟です。例外応答の例を次に示します。

{ "error": "Authentication token is not valid." }

ASP.NET MVCのシリアル化設定を使用してすべてのエンドポイントでのシリアル化の一貫性を向上させるために、応答オブジェクトをシリアル化するときにメソッドに注入IOptions<MvcJsonOptions>してInvoke使用することを検討してくださいJsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)

アプローチ2

UseExceptionHandler単純なシナリオで「正常」に機能する、別の非自明なAPIがあります。

app.UseExceptionHandler(a => a.Run(async context =>
{
    var feature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = feature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

これは非常に明白ではありませんが、例外処理を設定する簡単な方法です。ただし、必要な依存関係を注入する機能を使用してより多くの制御を取得できるため、それでもミドルウェアアプローチを優先します。


4
私は今日、カスタムミドルウェアを機能させるために机に頭をぶつけており、基本的には同じように機能します(リクエストの作業単位/トランザクションを管理するために使用しています)。私が直面している問題は、「次へ」で発生した例外がミドルウェアでキャッチされないことです。ご想像のとおり、これには問題があります。私は何をしているのですか?ポインタや提案はありますか?
brappleye3 2017

5
@ brappleye3-私は問題が何であるかを理解しました。Startup.csクラスの間違った場所にミドルウェアを登録したところです。app.UseMiddleware<ErrorHandlingMiddleware>();直前に引っ越しましたapp.UseStaticFiles();。例外は現在正しくキャッチされているようです。これはapp.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();、ミドルウェアの順序を正しくするために内部の魔法のミドルウェアハッカーを実行することを私に信じさせる。
ジャマダン

4
カスタムミドルウェアは非常に有用である可能性があることには同意しますが、NotFound、Unauthorized、BadRequestの状況の例外を使用すると疑問が生じます。(NotFound()などを使用して)ステータスコードを設定し、カスタムミドルウェアまたはUseStatusCodePagesWithReExecuteで処理するのはなぜですか?詳細については、devtrends.co.uk / blog / handling
Paul

4
コンテンツのネゴシエーションを完全に無視して、常にJSONにシリアル化しているのは悪いことです。
Konrad、

5
@Konradの有効なポイント。だからこそ、この例はあなたが始めることができる場所であり、最終結果ではない、と述べました。APIの99%ではJSONで十分です。この答えでは不十分だと思われる場合は、貢献してください。
Andrei

60

最新Asp.Net Core(少なくとも2.2から、おそらく以前のバージョン)にはミドルウェアが組み込まれており、受け入れられた回答の実装と比べて少し簡単です。

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

それはほとんど同じことをするはずです、書くコードが少し少ないだけです。

重要:順序が重要であるため、前にUseMvc(またはUseRouting.Net Core 3で)追加することを忘れないでください。


ハンドラーへの引数としてDIをサポートしていますか、それともハンドラー内でサービスロケーターパターンを使用する必要がありますか?
lp

33

最善の策は、ミドルウェアを使用して、探しているロギングを実現することです。例外ロギングを1つのミドルウェアに配置し、ユーザーに表示されるエラーページを別のミドルウェアで処理します。これにより、ロジックの分離が可能になり、Microsoftが2つのミドルウェアコンポーネントを使用してレイアウトした設計に従います。Microsoftのドキュメントへの適切なリンクは次のとおりです。ASP.NetCoreでのエラー処理

具体的な例としては、StatusCodePageミドルウェアの拡張機能の1つを使用するか、このように独自に拡張することができます。

例外のロギングの例は、ここにあります:ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

その特定の実装が気に入らない場合は、ELMミドルウェアを使用することもできます。次にいくつかの例を示します。Elm例外ミドルウェア

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

それでもうまくいかない場合は、ExceptionHandlerMiddlewareとElmMiddlewareの実装を調べて独自のミドルウェアコンポーネントを作成し、独自のミドルウェアコンポーネントを構築するための概念を把握できます。

StatusCodePagesミドルウェアの下に、他のすべてのミドルウェアコンポーネントの上に、例外処理ミドルウェアを追加することが重要です。このようにして、例外ミドルウェアが例外をキャプチャしてログに記録し、リクエストがStatusCodePageミドルウェアに進むことを許可します。これにより、ユーザーにわかりやすいエラーページが表示されます。


どういたしまして。また、リクエストをより適切に満たすことができるエッジケースでデフォルトのUseStatusPagesをオーバーライドする例へのリンクも提供しました。
アシュリーリー

1
Elmはログを永続化しないため、シリアル化を提供するためにSerilogまたはNLogを使用することをお勧めします。ELMログが消える
Michael Freidgeim 2017

2
リンクが壊れています。
Mathias Lykkegaard Lorenzen

@AshleyLee、私はそれUseStatusCodePagesがWeb APIサービス実装で使用されていることを疑います。ビューやHTMLはまったくなく、JSON応答のみ...
Paul Michalik

23

よく受け入れられた回答は私に大いに役立ちましたが、実行時にエラーステータスコードを管理するために、ミドルウェアにHttpStatusCodeを渡したかったです。

このリンクによると私は同じことをするいくつかのアイデアを得ました。そこで、Andrei Answerをこれにマージしました。だから私の最終的なコードは以下です:
1.基本クラス

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

2.カスタム例外クラスタイプ

 public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}


3.カスタム例外ミドルウェア

public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)exception.StatusCode }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() { Message = "Runtime Error", StatusCode = (int)HttpStatusCode.BadRequest }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)HttpStatusCode.InternalServerError }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}


4.拡張方法

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<CustomExceptionMiddleware>();
    }

5. startup.csでメソッドを構成する

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

アカウントコントローラのログインメソッド:

 try
        {
            IRepository<UserMaster> obj = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
            var Result = obj.Get().AsQueryable().Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() && sb.Password == objData.Password.ToEncrypt() && sb.Status == (int)StatusType.Active).FirstOrDefault();
            if (Result != null)//User Found
                return Result;
            else// Not Found
                throw new HttpStatusCodeException(HttpStatusCode.NotFound, "Please check username or password");
        }
        catch (Exception ex)
        {
            throw ex;
        }

上記では、ユーザーが見つからなかったかどうかを確認し、HttpStatusCode.NotFoundステータスとカスタムメッセージを渡したHttpStatusCodeExceptionを発生させています
。ミドルウェア

キャッチ(HttpStatusCodeException ex)

制御が渡されるブロックされたが呼び出されます

プライベートタスクHandleExceptionAsync(HttpContext context、HttpStatusCodeException exception)メソッド




しかし、以前にランタイムエラーが発生した場合はどうなりますか?そのため、私は例外をスローし、catch(Exception exceptionObj)ブロックでキャッチされ、制御をに渡すtry catchブロックを使用しました

タスクHandleExceptionAsync(HttpContextコンテキスト、例外例外)

方法。

統一のために単一のErrorDetailsクラスを使用しました。


拡張メソッドをどこに置くか?残念ながら、中startup.csvoid Configure(IapplicationBuilder app)、私はエラーを取得しますIApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware。そして、参照を追加しましたCustomExceptionMiddleware.cs
Spedo De La Rossa

APIの速度が低下するため、例外を使用したくありません。例外は非常に高価です。
lnaie

@Inaie、それについては言えません...しかし、あなたは処理する例外を得たことはないようです...素晴らしい仕事
Arjun

19

例外タイプごとの例外処理動作を設定するには、NuGetパッケージのミドルウェアを使用できます。

コードサンプル:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}

16

まず、彼の例に基づいて解決策を説明してくれたAndreiに感謝します。

より完全なサンプルであり、読者の時間を節約できる可能性があるため、私は私のものを含めています。

Andreiのアプローチの制限は、ロギングを処理せず、潜在的に有用な要求変数とコンテンツネゴシエーションをキャプチャすることです(クライアントが要求したものに関係なく常にJSONを返します-XML /プレーンテキストなど)。

私のアプローチは、MVCに組み込まれた機能を使用できるようにするObjectResultを使用することです。

このコードは、応答のキャッシュも防止します。

エラー応答は、XMLシリアライザーによってシリアル化できるように装飾されています。

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}

9

まず、ASP.NET Core 2 Startupを構成して、Webサーバーからのエラーや未処理の例外のエラーページを再実行します。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

次に、HTTPステータスコードでエラーをスローできる例外タイプを定義します。

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

最後に、エラーページのコントローラーで、エラーの理由と、エンドユーザーが直接応答を表示するかどうかに基づいて、応答をカスタマイズします。このコードは、すべてのAPI URLがで始まることを前提としています/api/

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

ASP.NET Coreは、デバッグするためにエラーの詳細をログに記録するため、(信頼できない可能性のある)リクエスタに提供したいのはステータスコードだけです。より多くの情報を表示したい場合は、拡張HttpExceptionして提供することができます。APIエラーの場合、あなたは置き換えることによって、メッセージ本文にJSONエンコードされたエラー情報を置くことができreturn StatusCode...return Json...


0

ミドルウェアを使用するか、IExceptionHandlerPathFeatureで問題ありません。eショップには別の方法があります

例外フィルターを作成して登録する

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.