長年、通常の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
};
}
}
そして、私のStartupフィルタの登録は以下の通りです。
services.AddMvc(options =>
{
options.Filters.Add(new AuthorizationFilter());
options.Filters.Add(new ErrorHandlingFilter());
});
私が抱えていた問題は、AuthorizationFilter
で例外が発生したときに、ErrorHandlingFilter
で処理されないことです。古いASP.NET Web APIで動作していたように、そこで捕らえられることを期待していました。
では、アクションフィルタからの例外だけでなく、すべてのアプリケーションの例外をキャッチするにはどうすればよいのでしょうか?
さまざまな例外処理方法を試した結果、私はミドルウェアを使うことにしました。私の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." }
JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)で ASP.NET MVC'のシリアライズ設定を利用して、すべてのエンドポイントでシリアライズの一貫性を高めるために、
IOptionsを
Invoke` メソッドに注入して、レスポンスオブジェクトをシリアライズするときにそれを使うことを検討してください。
他にも、UseExceptionHandler
という目立たないAPIがありますが、これは単純なscenariousなものであれば問題なく動作します。
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);
}));
これはあまり目立たないものですが、例外処理を設定するには簡単な方法です。しかし、必要な依存関係を注入することでより多くの制御が可能になるため、私はまだミドルウェアのアプローチを好んでいます。
求めているロギングを実現するには、ミドルウェアを使うのが一番です。 あるミドルウェアで例外のロギングを行い、別のミドルウェアでユーザーに表示されるエラーページを処理したいのです。 これにより、ロジックの分離が可能になり、Microsoftが2つのミドルウェアコンポーネントを使って設計したデザインに従うことができます。 Microsoftのドキュメントへの良いリンクがあります。ASP.Net Coreにおけるエラー処理。
あなたの具体的な例では、[StatusCodePageミドルウェア][2]の拡張機能の1つを使用するか、thisのように独自に開発するとよいでしょう。
例外のログを取る例はこちらにあります。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 Middlewareを使用することもでき、ここにいくつかの例があります。Elm Exception Middlewareを参照してください。
public void Configure(IApplicationBuilder app)
{
app.UseStatusCodePagesWithReExecute("/Errors/{0}");
// Exception handling logging below
app.UseElmCapture();
app.UseElmPage();
}
それがうまくいかない場合は、独自のミドルウェアコンポーネントを作ることもできます。
例外処理ミドルウェアは、StatusCodePagesミドルウェアの下に、他のミドルウェアコンポーネントの上に追加することが重要です。 これにより、Exceptionミドルウェアは例外を捕捉し、ログに記録し、リクエストをStatusCodePageミドルウェアに進めることができ、ユーザーにフレンドリーなエラーページを表示します。
まず、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エラーについては、return StatusCode...
をreturn Json...
に置き換えることで、JSONにエンコードされたエラー情報をメッセージボディに入れることができます。