events
ASP.NET Core のエラーを処理する
注意
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
著者: Tom Dykstra
この記事では、ASP.NET Core Web アプリでエラーを処理するための一般的な手法について取り上げます。 「ASP.NET Core コントローラーベースの Web API でのエラー処理」および「Minimal API でのエラー処理」も参照してください。
この記事のガイダンスに追加されるまたは優先する Blazor エラー処理のガイダンスについては、「ASP.NET Core Blazor アプリのエラーを処理する」をご覧ください。
"開発者例外ページ" には、未処理の要求の例外に関する詳細な情報が表示されます。 DeveloperExceptionPageMiddleware を使用して、HTTP パイプラインから同期および非同期例外をキャプチャし、エラー応答を生成します。 開発者例外ページは、後続のミドルウェアでスローされた未処理の例外をキャッチできるように、ミドルウェア パイプラインの早い段階で実行されます。
次の両方が当てはまる場合、ASP.NET Core アプリでは既定で開発者例外ページが有効になります。
- 開発環境で実行している。
- アプリが現在のテンプレート、つまり WebApplication.CreateBuilder を使用して作成された。
以前のテンプレート、つまり 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
運用環境のカスタム エラー処理ページを構成するには、UseExceptionHandler を呼び出します。 この例外処理ミドルウェアは、次のことを行います。
- 未処理の例外をキャッチしてログに記録します。
- 指定されたパスを使用して、別のパイプラインで要求を再実行します。 応答が始まっていた場合、要求は再実行されません。 テンプレートによって生成されたコードは、
/Error
パスを使用して要求を再実行します。
警告
代替パイプラインで独自の例外がスローされた場合、例外処理ミドルウェアによって元の例外が再スローされます。
このミドルウェアは要求パイプラインを再実行できるため:
- ミドルウェアは、同じ要求での再入を処理する必要があります。 これは通常、
_next
を呼び出した後で状態をクリーンアップすること、またはやり直しを避けるため処理をHttpContext
にキャッシュすることを意味します。 要求本文を処理するときは、これはフォーム リーダーのような結果をバッファーまたはキャッシュすることを意味します。 - テンプレートで使われる UseExceptionHandler(IApplicationBuilder, String) オーバーロードの場合、要求パスのみが変更され、ルート データはクリアされます。 ヘッダー、メソッド、項目などの要求データは、すべてそのまま再利用されます。
- スコープ付きサービスは同じままです。
次の例では、UseExceptionHandler により非開発環境に例外処理ミドルウェアが追加されます。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
Razor Pages アプリのテンプレートには、エラー ページ (.cshtml
) と PageModel クラス (ErrorModel
) が Pages フォルダー内に用意されています。 MVC アプリの場合、プロジェクト テンプレートには、Error
コントローラー用の Home アクション メソッドとエラー ビューが含まれています。
例外処理ミドルウェアでは、"元の" HTTP メソッドを使用して要求が再実行されます。 エラー ハンドラーのエンドポイントが特定の HTTP メソッドのセットに制限されている場合は、それらの HTTP メソッドに対してのみ実行されます。 たとえば、[HttpGet]
属性を使用する MVC コントローラーのアクションは、GET 要求に対してのみ実行されます。 "すべての" 要求がカスタム エラー処理ページに到達するようにするために、それらを特定の HTTP メソッドのセットに制限しないでください。
元の HTTP メソッドに応じて例外を異なる方法で処理するには:
- Razor Pages の場合は、複数のハンドラー メソッドを作成します。 たとえば、GET 例外を処理するために
OnGet
を使用し、POST 例外を処理するためにOnPost
を使用します。 - MVC の場合は、複数のアクションに HTTP 動詞属性を適用します。 たとえば、GET 例外を処理するために
[HttpGet]
を使用し、POST 例外を処理するために[HttpPost]
を使用します。
認証されていないユーザーがカスタム エラー処理ページを表示できるようにするには、匿名アクセスがサポートされるようにします。
エラー ハンドラーで例外や元の要求パスにアクセスするには、IExceptionHandlerPathFeature を使います。 次の例では、IExceptionHandlerPathFeature
を使用して、スローされた例外に関する詳細を取得しています。
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public string? ExceptionMessage { get; set; }
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
ExceptionMessage = "The file was not found.";
}
if (exceptionHandlerPathFeature?.Path == "/")
{
ExceptionMessage ??= string.Empty;
ExceptionMessage += " Page: Home.";
}
}
}
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
カスタム例外ハンドラー ページの代わりになるのは、UseExceptionHandler にラムダを提供することです。 ラムダを使うと、応答を返す前にエラーにアクセスできます。
次のコードは、例外処理にラムダを使用しています。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
// using static System.Net.Mime.MediaTypeNames;
context.Response.ContentType = Text.Plain;
await context.Response.WriteAsync("An exception was thrown.");
var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
await context.Response.WriteAsync(" The file was not found.");
}
if (exceptionHandlerPathFeature?.Path == "/")
{
await context.Response.WriteAsync(" Page: Home.");
}
});
});
app.UseHsts();
}
ラムダを使用するもう 1 つの方法は、次の例のように、例外の種類に基づいて状態コードを設定することです。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseExceptionHandler(new ExceptionHandlerOptions
{
StatusCodeSelector = ex => ex is TimeoutException
? StatusCodes.Status503ServiceUnavailable
: StatusCodes.Status500InternalServerError
});
}
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
IExceptionHandler は、既知の例外を処理するコールバックを、一元化された場所で開発者に提供するインターフェイスです。
IExceptionHandler
の実装は、IServiceCollection.AddExceptionHandler<T>
を呼び出すことで登録されます。 IExceptionHandler
インスタンスの有効期間はシングルトンです。 複数の実装を追加することが可能で、登録された順序で呼び出されます。
例外ハンドラーが要求を処理する場合は、true
を返して処理を停止できます。 例外ハンドラーによって例外が処理されない場合、ミドルウェアの既定の動作とオプションに制御がフォールバックされます。 処理された例外と未処理の例外では、異なるメトリックとログが出力されます。
次の例は IExceptionHandler
の実装を示しています。
using Microsoft.AspNetCore.Diagnostics;
namespace ErrorHandlingSample
{
public class CustomExceptionHandler : IExceptionHandler
{
private readonly ILogger<CustomExceptionHandler> logger;
public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
{
this.logger = logger;
}
public ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
var exceptionMessage = exception.Message;
logger.LogError(
"Error Message: {exceptionMessage}, Time of occurrence {time}",
exceptionMessage, DateTime.UtcNow);
// Return false to continue with the default behavior
// - or - return true to signal that this exception is handled
return ValueTask.FromResult(false);
}
}
}
次の例は、依存性の注入で IExceptionHandler
の実装を登録する方法を示しています。
using ErrorHandlingSample;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// Remaining Program.cs code omitted for brevity
上記のコードが開発環境で実行されている場合:
- 例外を処理するために
CustomExceptionHandler
がまず呼び出されます。 - 例外をログに記録した後、
TryHandleAsync
メソッドはfalse
を返します。そのため、開発者例外ページが表示されます。
その他の環境:
- 例外を処理するために
CustomExceptionHandler
がまず呼び出されます。 - 例外をログに記録した後、
TryHandleAsync
メソッドはfalse
を返します。そのため、/Error
ページが表示されます。
ASP.NET Core アプリでは、既定で、"404 - 見つかりません" などの HTTP エラー状態コードの状態コード ページが表示されません。 アプリで、本文のない HTTP 400 から 599 のエラー状態コードが設定されると、状態コードと空の応答本文が返されます。 一般的なエラー状態コード用に既定のテキスト専用ハンドラーを有効にするには、UseStatusCodePages で Program.cs
を呼び出します。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages();
要求処理ミドルウェアの前に UseStatusCodePages
を呼び出します。 たとえば、静的ファイル ミドルウェアとエンドポイント ミドルウェアの前に UseStatusCodePages
を呼び出します。
UseStatusCodePages
を使用しない場合、エンドポイントなしで URL に移動すると、エンドポイントが見つからないことを示すブラウザー依存のエラー メッセージが返されます。 UseStatusCodePages
が呼び出されると、ブラウザーにより次の応答が返されます。
Status Code: 404; Not Found
UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
注意
状態コード ページのミドルウェアは例外をキャッチしません。 カスタム エラー処理ページを提供するには、例外ハンドラー ページを使用します。
応答の内容の種類とテキストをカスタマイズするには、内容の種類と書式文字列を受け取る UseStatusCodePages のオーバーロードを使います。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");
上記のコードでは、{0}
はエラー コードのプレースホルダーです。
書式指定文字列を含む UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
カスタム エラー処理と応答書き込みコードを指定するには、ラムダ式を受け取る UseStatusCodePages のオーバーロードを使います。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages(async statusCodeContext =>
{
// using static System.Net.Mime.MediaTypeNames;
statusCodeContext.HttpContext.Response.ContentType = Text.Plain;
await statusCodeContext.HttpContext.Response.WriteAsync(
$"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
ラムダを含む UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
UseStatusCodePagesWithRedirects 拡張メソッド:
- クライアントに 302 - Found 状態コードを送信します。
- URL テンプレートで指定されているエラー処理エンドポイントにクライアントをリダイレクトします。 エラー処理エンドポイントには、通常、エラー情報が表示され、HTTP 200 が返されます。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
前のコードに示されているように、URL テンプレートには状態コード用の {0}
プレースホルダーを含めることができます。 URL テンプレートが ~
(チルダ) で始まっている場合、~
はアプリの PathBase
に置き換えられます。 アプリでエンドポイントを指定するときに、そのエンドポイントの MVC ビューまたは Razor ページを作成します。
この方法は、次のようなアプリで一般的に使用されます。
- クライアントを別のエンドポイントにリダイレクトする必要がある場合 (通常は、別のアプリがエラーを処理する場合)。 Web アプリの場合は、クライアントのブラウザーのアドレス バーにリダイレクトされたエンドポイントが反映されます。
- 元の状態コードを保持し、初回のリダイレクト応答で返してはいけない場合。
UseStatusCodePagesWithReExecute 拡張メソッド:
- 代替パスを使用して要求パイプラインを再実行することで、応答本文を生成します。
- パイプラインの再実行前または再実行後に状態コードを変更しません。
新しいパイプラインでは状態コードが完全に制御されるため、新しいパイプラインの実行によって応答の状態コードが変更される可能性があります。 新しいパイプラインで状態コードが変更されない場合、元の状態コードがクライアントに送信されます。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
アプリ内でエンドポイントが指定されている場合は、そのエンドポイントの MVC ビューまたは Razor ページを作成します。
この方法は、次のようなアプリで一般的に使用されます。
- 別のエンドポイントにリダイレクトすることなく要求を処理する。 Web アプリの場合は、クライアントのブラウザーのアドレス バーに、初めに要求されていたエンドポイントが反映されます。
- 元の状態コードを保持し、応答で返す。
URL テンプレートは /
で始まる必要があります。このテンプレートには、状態コード用のプレースホルダー {0}
を含めることができます。 状態コードをクエリ文字列パラメーターとして渡すには、2 番目の引数を UseStatusCodePagesWithReExecute
に渡します。 次に例を示します。
var app = builder.Build();
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");
次の例で示すように、エラーを処理するエンドポイントでは、エラーを生成した元の URL を取得できます。
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
public int OriginalStatusCode { get; set; }
public string? OriginalPathAndQuery { get; set; }
public void OnGet(int statusCode)
{
OriginalStatusCode = statusCode;
var statusCodeReExecuteFeature =
HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
if (statusCodeReExecuteFeature is not null)
{
OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
+ $"{statusCodeReExecuteFeature.OriginalPath}"
+ $"{statusCodeReExecuteFeature.OriginalQueryString}";
}
}
}
このミドルウェアは要求パイプラインを再実行できるため:
- ミドルウェアは、同じ要求での再入を処理する必要があります。 これは通常、
_next
を呼び出した後で状態をクリーンアップすること、またはやり直しを避けるため処理をHttpContext
にキャッシュすることを意味します。 要求本文を処理するときは、これはフォーム リーダーのような結果をバッファーまたはキャッシュすることを意味します。 - スコープ付きサービスは同じままです。
MVC コントローラーまたはアクション メソッドの状態コード ページを無効にするには、[SkipStatusCodePages] 属性を使用します。
Razor Pages ハンドラー メソッドまたは MVC コントローラーの特定の要求に対して状態コード ページを無効にするには、IStatusCodePagesFeature を使用します。
public void OnGet()
{
var statusCodePagesFeature =
HttpContext.Features.Get<IStatusCodePagesFeature>();
if (statusCodePagesFeature is not null)
{
statusCodePagesFeature.Enabled = false;
}
}
例外処理ページのコードが例外をスローすることもあります。 運用環境のエラー ページは十分にテストし、それ自体から例外がスローされないように特に注意する必要があります。
応答のヘッダーが送信された後は、次のようになります。
- アプリで応答の状態コードを変更できません。
- すべての例外ページやハンドラーを実行できません。 応答は完了している必要があります。あるいは、接続が中止となっている必要があります。
アプリ内の例外処理ロジックに加えて、HTTP サーバーの実装でも一部の例外を処理できます。 応答ヘッダーの送信前にサーバーで例外がキャッチされると、サーバーによって "500 - Internal Server Error
" 応答が応答本文なしで送信されます。 応答ヘッダーの送信後にサーバーで例外がキャッチされた場合、サーバーは接続を閉じます。 アプリで処理されない要求はサーバーで処理されます。 サーバーが要求を処理しているときに発生した例外は、すべてサーバーの例外処理によって処理されます。 この動作は、アプリのカスタム エラー ページ、例外処理ミドルウェア、およびフィルターから影響を受けません。
アプリの起動中に起こる例外はホスティング層だけが処理できます。 起動時のエラーをキャプチャしたり、詳細なエラーをキャプチャしたりするように、ホストを構成することができます。
ホスティング レイヤーでは、ホスト アドレス/ポート バインド後にエラーが発生した場合にのみ、キャプチャされた起動時エラーに対するエラー ページを表示できます。 バインドが失敗した場合は、次のようになります。
- ホスティング レイヤーにより重大な例外がログに記録されます。
- dotnet プロセスがクラッシュします。
- HTTP サーバーが Kestrel のときは、エラー ページは表示されません。
IIS (または Azure App Service) または IIS Express 上で実行している場合、プロセスを開始できなければ、ASP.NET Core モジュールから "502.5 - 処理エラー" が返されます。 詳細については、「Azure App Service および IIS での ASP.NET Core のトラブルシューティング」を参照してください。
データベース開発者ページ例外フィルター AddDatabaseDeveloperPageExceptionFilter では、Entity Framework Core の移行を使って解決できるデータベース関連の例外がキャプチャされます。 これらの例外が発生すると、問題が解決する可能性のあるアクションの詳細を含む HTML 応答が生成されます。 このページは、開発環境でのみ有効です。 次のコードでは、データベース開発者ページの例外フィルターを追加しています。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
MVC アプリでは、例外フィルターをグローバルに、またはコントローラーやアクションの単位で構成できます。 Razor Pages アプリでは、グローバルに、またはページ モデルの単位で構成できます。 このようなフィルターは、コントローラー アクションや別のフィルターの実行中に発生する未処理の例外を処理します。 詳細については、「ASP.NET Core フィルター」を参照してください。
例外フィルターは、MVC アクション内で発生する例外をトラップする場合には便利ですが、組み込みの例外処理ミドルウェアUseExceptionHandler ほど柔軟ではありません。 選択された MVC アクションに応じて異なる方法でエラー処理を実行する必要がある場合を除き、UseExceptionHandler
を使用することをお勧めします。
モデル状態エラーを処理する方法については、モデル バインドおよびモデルの検証に関する記事をご覧ください。
問題の詳細は、HTTP API エラーを記述する唯一の応答形式ではありませんが、一般的に HTTP API のエラーを報告するために使用されます。
問題の詳細サービスは、IProblemDetailsService インターフェイスを実装し、これにより、ASP.NET Core での問題の詳細の作成がサポートされます。 AddProblemDetails(IServiceCollection) の IServiceCollection 拡張メソッドは、既定の IProblemDetailsService
実装を登録します。
ASP.NET Core アプリでは、次のミドルウェアによって、AddProblemDetails
が呼び出されたときに問題の詳細 HTTP 応答が生成されます。ただし、Accept
要求 HTTP ヘッダーに、登録された IProblemDetailsWriter (既定値: application/json
) によってサポートされるいずれかのコンテンツ タイプが含まれていない場合を除きます。
- ExceptionHandlerMiddleware: カスタム ハンドラーが定義されていない場合に、問題の詳細の応答を生成します。
- StatusCodePagesMiddleware: 既定で問題の詳細の応答を生成します。
- DeveloperExceptionPageMiddleware: 開発中に
Accept
要求 HTTP ヘッダーにtext/html
が含まれていない場合に、問題の詳細応答を生成します。
次のコードは、すべての HTTP クライアント用の "本文コンテンツがまだ含まれていない" 問題の詳細応答とサーバー エラー応答を生成するようにアプリを構成します。
builder.Services.AddProblemDetails();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
app.UseHsts();
}
app.UseStatusCodePages();
次のセクションでは、問題の詳細の応答の本文をカスタマイズする方法を示します。
ProblemDetails
の自動作成は、次のどのオプションでもカスタマイズできます。
ProblemDetailsOptions.CustomizeProblemDetails
を使用します- カスタム
IProblemDetailsWriter
を使用する - ミドルウェアで
IProblemDetailsService
を呼び出す
生成された問題の詳細は CustomizeProblemDetails を使用してカスタマイズでき、カスタマイズはすべての自動生成された問題の詳細に適用されます。
次のコードは ProblemDetailsOptions を使用して CustomizeProblemDetails を設定します。
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = ctx =>
ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
app.UseHsts();
}
app.UseStatusCodePages();
たとえば、HTTP Status 400 Bad Request
エンドポイントの結果により、次の問題の詳細の応答本文が生成されます。
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Bad Request",
"status": 400,
"nodeId": "my-machine-name"
}
高度なカスタマイズのために IProblemDetailsWriter の実装を作成できます。
public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
// Indicates that only responses with StatusCode == 400
// are handled by this writer. All others are
// handled by different registered writers if available.
public bool CanWrite(ProblemDetailsContext context)
=> context.HttpContext.Response.StatusCode == 400;
public ValueTask WriteAsync(ProblemDetailsContext context)
{
// Additional customizations.
// Write to the response.
var response = context.HttpContext.Response;
return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
}
}
注: カスタムの IProblemDetailsWriter
を使う場合は、IProblemDetailsWriter
、AddRazorPages、AddControllers、または AddControllersWithViews を呼び出す前にカスタムの AddMvc を登録する必要があります。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();
var app = builder.Build();
// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
await next(context);
var mathErrorFeature = context.Features.Get<MathErrorFeature>();
if (mathErrorFeature is not null)
{
if (context.RequestServices.GetService<IProblemDetailsWriter>() is
{ } problemDetailsService)
{
if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
{
(string Detail, string Type) details = mathErrorFeature.MathError switch
{
MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
"https://en.wikipedia.org/wiki/Division_by_zero"),
_ => ("Negative or complex numbers are not valid input.",
"https://en.wikipedia.org/wiki/Square_root")
};
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = "Bad Input",
Detail = details.Detail,
Type = details.Type
}
});
}
}
}
});
// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
if (denominator == 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.DivisionByZeroError
};
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(numerator / denominator);
});
// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
if (radicand < 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.NegativeRadicandError
};
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(Math.Sqrt(radicand));
});
app.Run();
ProblemDetailsOptions を CustomizeProblemDetails と共に使用する別の方法として、ミドルウェアで ProblemDetails を設定します。 問題の詳細の応答は、IProblemDetailsService.WriteAsync
を呼び出すことによって書き込むことができます。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStatusCodePages();
// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
await next(context);
var mathErrorFeature = context.Features.Get<MathErrorFeature>();
if (mathErrorFeature is not null)
{
if (context.RequestServices.GetService<IProblemDetailsService>() is
{ } problemDetailsService)
{
(string Detail, string Type) details = mathErrorFeature.MathError switch
{
MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
"https://en.wikipedia.org/wiki/Division_by_zero"),
_ => ("Negative or complex numbers are not valid input.",
"https://en.wikipedia.org/wiki/Square_root")
};
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = "Bad Input",
Detail = details.Detail,
Type = details.Type
}
});
}
}
});
// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
if (denominator == 0)
{
var errorType = new MathErrorFeature { MathError =
MathErrorType.DivisionByZeroError };
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(numerator / denominator);
});
// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
if (radicand < 0)
{
var errorType = new MathErrorFeature { MathError =
MathErrorType.NegativeRadicandError };
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(Math.Sqrt(radicand));
});
app.MapControllers();
app.Run();
前のコードでは、最小限の API エンドポイント /divide
および /squareroot
が、エラー入力時に予期されるカスタム問題の応答を返します。
API コントローラー エンドポイントは、カスタムの問題の応答ではなく、エラー入力で既定の問題の応答を返します。 既定の問題の応答が返されるのは、 が呼び出される前で、応答が再度IProblemDetailsService.WriteAsync
うちに、API コントローラーがエラー状態コードの問題の詳細を応答ストリームに書き込んでいるためです。
次の ValuesController
は BadRequestResult を返します。これは、応答ストリームに書き込むため、カスタムの問題の応答が返されるのを回避します。
[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));
}
}
次の Values3Controller
は ControllerBase.Problem
を返すため、予期されるカスタム問題の結果が返されます。
[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
// /api/values3/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 Problem(
title: "Bad Input",
detail: "Divison by zero is not defined.",
type: "https://en.wikipedia.org/wiki/Division_by_zero",
statusCode: StatusCodes.Status400BadRequest
);
}
return Ok(Numerator / Denominator);
}
// /api/values3/squareroot/4
[HttpGet("{radicand}")]
public IActionResult Squareroot(double radicand)
{
if (radicand < 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.NegativeRadicandError
};
HttpContext.Features.Set(errorType);
return Problem(
title: "Bad Input",
detail: "Negative or complex numbers are not valid input.",
type: "https://en.wikipedia.org/wiki/Square_root",
statusCode: StatusCodes.Status400BadRequest
);
}
return Ok(Math.Sqrt(radicand));
}
}
次のアプリを考えてみましょう。
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();
非開発環境では、例外が発生した場合、以下が、クライアントに返される標準の ProblemDetails 応答です。
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}
ほとんどのアプリでは、例外に必要なコードは上記ですべてです。 ただし、次のセクションでは、より詳細な問題の応答を取得する方法を示します。
カスタム例外ハンドラー ページの代わりになるのは、UseExceptionHandler にラムダを提供することです。 ラムダを使用すると、エラーにアクセスし、IProblemDetailsService.WriteAsync
を使用して問題の詳細の応答を書き込むことができます。
using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;
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();
}
else
{
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = Text.Plain;
var title = "Bad Input";
var detail = "Invalid input";
var type = "https://errors.example.com/badInput";
if (context.RequestServices.GetService<IProblemDetailsService>() is
{ } problemDetailsService)
{
var exceptionHandlerFeature =
context.Features.Get<IExceptionHandlerFeature>();
var exceptionType = exceptionHandlerFeature?.Error;
if (exceptionType != null &&
exceptionType.Message.Contains("infinity"))
{
title = "Argument exception";
detail = "Invalid input";
type = "https://errors.example.com/argumentException";
}
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = title,
Detail = detail,
Type = type
}
});
}
});
});
}
app.MapControllers();
app.Run();
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
問題の詳細を生成する別の方法としては、例外とクライアント エラーを問題の詳細にマップするために使用できるサードパーティの NuGet パッケージ Hellang.Middleware.ProblemDetails を使います。
著者: Tom Dykstra
この記事では、ASP.NET Core Web アプリでエラーを処理するための一般的な手法について取り上げます。 「ASP.NET Core コントローラーベースの Web API でのエラー処理」および「Minimal API でのエラー処理」も参照してください。
"開発者例外ページ" には、未処理の要求の例外に関する詳細な情報が表示されます。 次の両方が当てはまる場合、ASP.NET Core アプリでは既定で開発者例外ページが有効になります。
- 開発環境で実行している。
- 現在のテンプレート、つまり、WebApplication.CreateBuilder で作成されたアプリ。
WebHost.CreateDefaultBuilder
で作成されたアプリでは、app.UseDeveloperExceptionPage
でConfigure
を呼び出して開発者例外ページを有効にする必要があります。
開発者例外ページは、後続のミドルウェアでスローされた未処理の例外をキャッチできるように、ミドルウェア パイプラインの早い段階で実行されます。
アプリが運用環境で実行されている場合は、詳細な例外情報を公開しないようにしてください。 環境の構成について詳しくは、「ASP.NET Core で複数の環境を使用する」を参照してください。
開発者例外ページには、例外と要求に関する次の情報が含まれている場合があります。
- スタック トレース
- クエリ文字列のパラメーター (ある場合)
- Cookie (ある場合)
- ヘッダー
開発者例外ページで何らかの情報が提供されるとは限りません。 完全なエラー情報については、ログ記録に関するページを参照してください。
運用環境のカスタム エラー処理ページを構成するには、UseExceptionHandler を呼び出します。 この例外処理ミドルウェアは、次のことを行います。
- 未処理の例外をキャッチしてログに記録します。
- 指定されたパスを使用して、別のパイプラインで要求を再実行します。 応答が始まっていた場合、要求は再実行されません。 テンプレートによって生成されたコードは、
/Error
パスを使用して要求を再実行します。
警告
代替パイプラインで独自の例外がスローされた場合、例外処理ミドルウェアによって元の例外が再スローされます。
このミドルウェアは要求パイプラインを再実行できるため:
- ミドルウェアは、同じ要求での再入を処理する必要があります。 これは通常、
_next
を呼び出した後で状態をクリーンアップすること、またはやり直しを避けるため処理をHttpContext
にキャッシュすることを意味します。 要求本文を処理するときは、これはフォーム リーダーのような結果をバッファーまたはキャッシュすることを意味します。 - テンプレートで使われる UseExceptionHandler(IApplicationBuilder, String) オーバーロードの場合、要求パスのみが変更され、ルート データはクリアされます。 ヘッダー、メソッド、項目などの要求データは、すべてそのまま再利用されます。
- スコープ付きサービスは同じままです。
次の例では、UseExceptionHandler により非開発環境に例外処理ミドルウェアが追加されます。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
Razor Pages アプリのテンプレートには、エラー ページ (.cshtml
) と PageModel クラス (ErrorModel
) が Pages フォルダー内に用意されています。 MVC アプリの場合、プロジェクト テンプレートには、Error
コントローラー用の Home アクション メソッドとエラー ビューが含まれています。
例外処理ミドルウェアでは、"元の" HTTP メソッドを使用して要求が再実行されます。 エラー ハンドラーのエンドポイントが特定の HTTP メソッドのセットに制限されている場合は、それらの HTTP メソッドに対してのみ実行されます。 たとえば、[HttpGet]
属性を使用する MVC コントローラーのアクションは、GET 要求に対してのみ実行されます。 "すべての" 要求がカスタム エラー処理ページに到達するようにするために、それらを特定の HTTP メソッドのセットに制限しないでください。
元の HTTP メソッドに応じて例外を異なる方法で処理するには:
- Razor Pages の場合は、複数のハンドラー メソッドを作成します。 たとえば、GET 例外を処理するために
OnGet
を使用し、POST 例外を処理するためにOnPost
を使用します。 - MVC の場合は、複数のアクションに HTTP 動詞属性を適用します。 たとえば、GET 例外を処理するために
[HttpGet]
を使用し、POST 例外を処理するために[HttpPost]
を使用します。
認証されていないユーザーがカスタム エラー処理ページを表示できるようにするには、匿名アクセスがサポートされるようにします。
エラー ハンドラーで例外や元の要求パスにアクセスするには、IExceptionHandlerPathFeature を使います。 次の例では、IExceptionHandlerPathFeature
を使用して、スローされた例外に関する詳細を取得しています。
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public string? ExceptionMessage { get; set; }
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
ExceptionMessage = "The file was not found.";
}
if (exceptionHandlerPathFeature?.Path == "/")
{
ExceptionMessage ??= string.Empty;
ExceptionMessage += " Page: Home.";
}
}
}
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
カスタム例外ハンドラー ページの代わりになるのは、UseExceptionHandler にラムダを提供することです。 ラムダを使うと、応答を返す前にエラーにアクセスできます。
次のコードは、例外処理にラムダを使用しています。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
// using static System.Net.Mime.MediaTypeNames;
context.Response.ContentType = Text.Plain;
await context.Response.WriteAsync("An exception was thrown.");
var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
await context.Response.WriteAsync(" The file was not found.");
}
if (exceptionHandlerPathFeature?.Path == "/")
{
await context.Response.WriteAsync(" Page: Home.");
}
});
});
app.UseHsts();
}
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
IExceptionHandler は、既知の例外を一元的に処理するためのコールバックを開発者に提供するインターフェイスです。
IExceptionHandler
の実装は、IServiceCollection.AddExceptionHandler<T>
[IServiceCollection.AddExceptionHandler<T>
] を呼び出すことで登録されます。 IExceptionHandler
インスタンスの有効期間はシングルトンです。 複数の実装を追加することが可能で、登録された順序で呼び出されます。
例外ハンドラーが要求を処理する場合は、true
を返して処理を停止できます。 例外ハンドラーによって例外が処理されない場合、ミドルウェアの既定の動作とオプションに制御がフォールバックされます。 処理された例外と未処理の例外では、異なるメトリックとログが出力されます。
次の例は IExceptionHandler
の実装を示しています。
using Microsoft.AspNetCore.Diagnostics;
namespace ErrorHandlingSample
{
public class CustomExceptionHandler : IExceptionHandler
{
private readonly ILogger<CustomExceptionHandler> logger;
public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
{
this.logger = logger;
}
public ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
var exceptionMessage = exception.Message;
logger.LogError(
"Error Message: {exceptionMessage}, Time of occurrence {time}",
exceptionMessage, DateTime.UtcNow);
// Return false to continue with the default behavior
// - or - return true to signal that this exception is handled
return ValueTask.FromResult(false);
}
}
}
次の例は、依存性の注入で IExceptionHandler
の実装を登録する方法を示しています。
using ErrorHandlingSample;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// Remaining Program.cs code omitted for brevity
上記のコードが開発環境で実行されている場合:
- 例外を処理するために
CustomExceptionHandler
がまず呼び出されます。 - 例外をログに記録した後、
TryHandleException
メソッドはfalse
を返します。そのため、開発者例外ページが表示されます。
その他の環境:
- 例外を処理するために
CustomExceptionHandler
がまず呼び出されます。 - 例外をログに記録した後、
TryHandleException
メソッドはfalse
を返します。そのため、/Error
ページが表示されます。
ASP.NET Core アプリでは、既定で、"404 - 見つかりません" などの HTTP エラー状態コードの状態コード ページが表示されません。 アプリで、本文のない HTTP 400 から 599 のエラー状態コードが設定されると、状態コードと空の応答本文が返されます。 一般的なエラー状態コード用に既定のテキスト専用ハンドラーを有効にするには、UseStatusCodePages で Program.cs
を呼び出します。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages();
要求処理ミドルウェアの前に UseStatusCodePages
を呼び出します。 たとえば、静的ファイル ミドルウェアとエンドポイント ミドルウェアの前に UseStatusCodePages
を呼び出します。
UseStatusCodePages
を使用しない場合、エンドポイントなしで URL に移動すると、エンドポイントが見つからないことを示すブラウザー依存のエラー メッセージが返されます。 UseStatusCodePages
が呼び出されると、ブラウザーにより次の応答が返されます。
Status Code: 404; Not Found
UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
注意
状態コード ページのミドルウェアは例外をキャッチしません。 カスタム エラー処理ページを提供するには、例外ハンドラー ページを使用します。
応答の内容の種類とテキストをカスタマイズするには、内容の種類と書式文字列を受け取る UseStatusCodePages のオーバーロードを使います。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");
上記のコードでは、{0}
はエラー コードのプレースホルダーです。
書式指定文字列を含む UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
カスタム エラー処理と応答書き込みコードを指定するには、ラムダ式を受け取る UseStatusCodePages のオーバーロードを使います。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages(async statusCodeContext =>
{
// using static System.Net.Mime.MediaTypeNames;
statusCodeContext.HttpContext.Response.ContentType = Text.Plain;
await statusCodeContext.HttpContext.Response.WriteAsync(
$"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
ラムダを含む UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
UseStatusCodePagesWithRedirects 拡張メソッド:
- クライアントに 302 - Found 状態コードを送信します。
- URL テンプレートで指定されているエラー処理エンドポイントにクライアントをリダイレクトします。 エラー処理エンドポイントには、通常、エラー情報が表示され、HTTP 200 が返されます。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
前のコードに示されているように、URL テンプレートには状態コード用の {0}
プレースホルダーを含めることができます。 URL テンプレートが ~
(チルダ) で始まっている場合、~
はアプリの PathBase
に置き換えられます。 アプリでエンドポイントを指定するときに、そのエンドポイントの MVC ビューまたは Razor ページを作成します。
この方法は、次のようなアプリで一般的に使用されます。
- クライアントを別のエンドポイントにリダイレクトする必要がある場合 (通常は、別のアプリがエラーを処理する場合)。 Web アプリの場合は、クライアントのブラウザーのアドレス バーにリダイレクトされたエンドポイントが反映されます。
- 元の状態コードを保持し、初回のリダイレクト応答で返してはいけない場合。
UseStatusCodePagesWithReExecute 拡張メソッド:
- 代替パスを使用して要求パイプラインを再実行することで、応答本文を生成します。
- パイプラインの再実行前または再実行後に状態コードを変更しません。
新しいパイプラインでは状態コードが完全に制御されるため、新しいパイプラインの実行によって応答の状態コードが変更される可能性があります。 新しいパイプラインで状態コードが変更されない場合、元の状態コードがクライアントに送信されます。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
アプリ内でエンドポイントが指定されている場合は、そのエンドポイントの MVC ビューまたは Razor ページを作成します。
この方法は、次のようなアプリで一般的に使用されます。
- 別のエンドポイントにリダイレクトすることなく要求を処理する。 Web アプリの場合は、クライアントのブラウザーのアドレス バーに、初めに要求されていたエンドポイントが反映されます。
- 元の状態コードを保持し、応答で返す。
URL テンプレートは /
で始まる必要があります。このテンプレートには、状態コード用のプレースホルダー {0}
を含めることができます。 状態コードをクエリ文字列パラメーターとして渡すには、2 番目の引数を UseStatusCodePagesWithReExecute
に渡します。 次に例を示します。
var app = builder.Build();
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");
次の例で示すように、エラーを処理するエンドポイントでは、エラーを生成した元の URL を取得できます。
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
public int OriginalStatusCode { get; set; }
public string? OriginalPathAndQuery { get; set; }
public void OnGet(int statusCode)
{
OriginalStatusCode = statusCode;
var statusCodeReExecuteFeature =
HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
if (statusCodeReExecuteFeature is not null)
{
OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
+ $"{statusCodeReExecuteFeature.OriginalPath}"
+ $"{statusCodeReExecuteFeature.OriginalQueryString}";
}
}
}
このミドルウェアは要求パイプラインを再実行できるため:
- ミドルウェアは、同じ要求での再入を処理する必要があります。 これは通常、
_next
を呼び出した後で状態をクリーンアップすること、またはやり直しを避けるため処理をHttpContext
にキャッシュすることを意味します。 要求本文を処理するときは、これはフォーム リーダーのような結果をバッファーまたはキャッシュすることを意味します。 - スコープ付きサービスは同じままです。
MVC コントローラーまたはアクション メソッドの状態コード ページを無効にするには、[SkipStatusCodePages] 属性を使用します。
Razor Pages ハンドラー メソッドまたは MVC コントローラーの特定の要求に対して状態コード ページを無効にするには、IStatusCodePagesFeature を使用します。
public void OnGet()
{
var statusCodePagesFeature =
HttpContext.Features.Get<IStatusCodePagesFeature>();
if (statusCodePagesFeature is not null)
{
statusCodePagesFeature.Enabled = false;
}
}
例外処理ページのコードが例外をスローすることもあります。 運用環境のエラー ページは十分にテストし、それ自体から例外がスローされないように特に注意する必要があります。
応答のヘッダーが送信された後は、次のようになります。
- アプリで応答の状態コードを変更できません。
- すべての例外ページやハンドラーを実行できません。 応答は完了している必要があります。あるいは、接続が中止となっている必要があります。
アプリ内の例外処理ロジックに加えて、HTTP サーバーの実装でも一部の例外を処理できます。 応答ヘッダーの送信前にサーバーで例外がキャッチされると、サーバーによって "500 - Internal Server Error
" 応答が応答本文なしで送信されます。 応答ヘッダーの送信後にサーバーで例外がキャッチされた場合、サーバーは接続を閉じます。 アプリで処理されない要求はサーバーで処理されます。 サーバーが要求を処理しているときに発生した例外は、すべてサーバーの例外処理によって処理されます。 この動作は、アプリのカスタム エラー ページ、例外処理ミドルウェア、およびフィルターから影響を受けません。
アプリの起動中に起こる例外はホスティング層だけが処理できます。 起動時のエラーをキャプチャしたり、詳細なエラーをキャプチャしたりするように、ホストを構成することができます。
ホスティング レイヤーでは、ホスト アドレス/ポート バインド後にエラーが発生した場合にのみ、キャプチャされた起動時エラーに対するエラー ページを表示できます。 バインドが失敗した場合は、次のようになります。
- ホスティング レイヤーにより重大な例外がログに記録されます。
- dotnet プロセスがクラッシュします。
- HTTP サーバーが Kestrel のときは、エラー ページは表示されません。
IIS (または Azure App Service) または IIS Express 上で実行している場合、プロセスを開始できなければ、ASP.NET Core モジュールから "502.5 - 処理エラー" が返されます。 詳細については、「Azure App Service および IIS での ASP.NET Core のトラブルシューティング」を参照してください。
データベース開発者ページ例外フィルター AddDatabaseDeveloperPageExceptionFilter では、Entity Framework Core の移行を使って解決できるデータベース関連の例外がキャプチャされます。 これらの例外が発生すると、問題が解決する可能性のあるアクションの詳細を含む HTML 応答が生成されます。 このページは、開発環境でのみ有効です。 次のコードでは、データベース開発者ページの例外フィルターを追加しています。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
MVC アプリでは、例外フィルターをグローバルに、またはコントローラーやアクションの単位で構成できます。 Razor Pages アプリでは、グローバルに、またはページ モデルの単位で構成できます。 このようなフィルターは、コントローラー アクションや別のフィルターの実行中に発生する未処理の例外を処理します。 詳細については、「ASP.NET Core フィルター」を参照してください。
例外フィルターは、MVC アクション内で発生する例外をトラップする場合には便利ですが、組み込みの例外処理ミドルウェアUseExceptionHandler ほど柔軟ではありません。 選択された MVC アクションに応じて異なる方法でエラー処理を実行する必要がある場合を除き、UseExceptionHandler
を使用することをお勧めします。
モデル状態エラーを処理する方法については、モデル バインドおよびモデルの検証に関する記事をご覧ください。
問題の詳細は、HTTP API エラーを記述する唯一の応答形式ではありませんが、一般的に HTTP API のエラーを報告するために使用されます。
問題の詳細サービスは、IProblemDetailsService インターフェイスを実装し、これにより、ASP.NET Core での問題の詳細の作成がサポートされます。 AddProblemDetails(IServiceCollection) の IServiceCollection 拡張メソッドは、既定の IProblemDetailsService
実装を登録します。
ASP.NET Core アプリでは、次のミドルウェアによって、AddProblemDetails
が呼び出されたときに問題の詳細 HTTP 応答が生成されます。ただし、Accept
要求 HTTP ヘッダーに、登録された IProblemDetailsWriter (既定値: application/json
) によってサポートされるいずれかのコンテンツ タイプが含まれていない場合を除きます。
- ExceptionHandlerMiddleware: カスタム ハンドラーが定義されていない場合に、問題の詳細の応答を生成します。
- StatusCodePagesMiddleware: 既定で問題の詳細の応答を生成します。
- DeveloperExceptionPageMiddleware: 開発中に
Accept
要求 HTTP ヘッダーにtext/html
が含まれていない場合に、問題の詳細応答を生成します。
次のコードは、"本文コンテンツがまだ含まれていない" すべての HTTP クライアントおよびサーバー エラー応答に対して問題の詳細の応答を生成するようにアプリを構成します。
builder.Services.AddProblemDetails();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
app.UseHsts();
}
app.UseStatusCodePages();
次のセクションでは、問題の詳細の応答の本文をカスタマイズする方法を示します。
ProblemDetails
の自動作成は、次のどのオプションでもカスタマイズできます。
ProblemDetailsOptions.CustomizeProblemDetails
を使用します- カスタム
IProblemDetailsWriter
を使用する - ミドルウェアで
IProblemDetailsService
を呼び出す
生成された問題の詳細は CustomizeProblemDetails を使用してカスタマイズでき、カスタマイズはすべての自動生成された問題の詳細に適用されます。
次のコードは ProblemDetailsOptions を使用して CustomizeProblemDetails を設定します。
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = ctx =>
ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
app.UseHsts();
}
app.UseStatusCodePages();
たとえば、HTTP Status 400 Bad Request
エンドポイントの結果により、次の問題の詳細の応答本文が生成されます。
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Bad Request",
"status": 400,
"nodeId": "my-machine-name"
}
高度なカスタマイズのために IProblemDetailsWriter の実装を作成できます。
public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
// Indicates that only responses with StatusCode == 400
// are handled by this writer. All others are
// handled by different registered writers if available.
public bool CanWrite(ProblemDetailsContext context)
=> context.HttpContext.Response.StatusCode == 400;
public ValueTask WriteAsync(ProblemDetailsContext context)
{
// Additional customizations.
// Write to the response.
var response = context.HttpContext.Response;
return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
}
}
注: カスタムの IProblemDetailsWriter
を使う場合は、IProblemDetailsWriter
、AddRazorPages、AddControllers、または AddControllersWithViews を呼び出す前にカスタムの AddMvc を登録する必要があります。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();
var app = builder.Build();
// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
await next(context);
var mathErrorFeature = context.Features.Get<MathErrorFeature>();
if (mathErrorFeature is not null)
{
if (context.RequestServices.GetService<IProblemDetailsWriter>() is
{ } problemDetailsService)
{
if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
{
(string Detail, string Type) details = mathErrorFeature.MathError switch
{
MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
"https://en.wikipedia.org/wiki/Division_by_zero"),
_ => ("Negative or complex numbers are not valid input.",
"https://en.wikipedia.org/wiki/Square_root")
};
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = "Bad Input",
Detail = details.Detail,
Type = details.Type
}
});
}
}
}
});
// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
if (denominator == 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.DivisionByZeroError
};
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(numerator / denominator);
});
// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
if (radicand < 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.NegativeRadicandError
};
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(Math.Sqrt(radicand));
});
app.Run();
ProblemDetailsOptions を CustomizeProblemDetails と共に使用する別の方法として、ミドルウェアで ProblemDetails を設定します。 問題の詳細の応答は、IProblemDetailsService.WriteAsync
を呼び出すことによって書き込むことができます。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStatusCodePages();
// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
await next(context);
var mathErrorFeature = context.Features.Get<MathErrorFeature>();
if (mathErrorFeature is not null)
{
if (context.RequestServices.GetService<IProblemDetailsService>() is
{ } problemDetailsService)
{
(string Detail, string Type) details = mathErrorFeature.MathError switch
{
MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
"https://en.wikipedia.org/wiki/Division_by_zero"),
_ => ("Negative or complex numbers are not valid input.",
"https://en.wikipedia.org/wiki/Square_root")
};
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = "Bad Input",
Detail = details.Detail,
Type = details.Type
}
});
}
}
});
// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
if (denominator == 0)
{
var errorType = new MathErrorFeature { MathError =
MathErrorType.DivisionByZeroError };
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(numerator / denominator);
});
// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
if (radicand < 0)
{
var errorType = new MathErrorFeature { MathError =
MathErrorType.NegativeRadicandError };
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(Math.Sqrt(radicand));
});
app.MapControllers();
app.Run();
前のコードでは、最小限の API エンドポイント /divide
および /squareroot
が、エラー入力時に予期されるカスタム問題の応答を返します。
API コントローラー エンドポイントは、カスタムの問題の応答ではなく、エラー入力で既定の問題の応答を返します。 既定の問題の応答が返されるのは、 が呼び出される前で、応答が再度IProblemDetailsService.WriteAsync
うちに、API コントローラーがエラー状態コードの問題の詳細を応答ストリームに書き込んでいるためです。
次の ValuesController
は BadRequestResult を返します。これは、応答ストリームに書き込むため、カスタムの問題の応答が返されるのを回避します。
[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));
}
}
次の Values3Controller
は ControllerBase.Problem
を返すため、予期されるカスタム問題の結果が返されます。
[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
// /api/values3/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 Problem(
title: "Bad Input",
detail: "Divison by zero is not defined.",
type: "https://en.wikipedia.org/wiki/Division_by_zero",
statusCode: StatusCodes.Status400BadRequest
);
}
return Ok(Numerator / Denominator);
}
// /api/values3/squareroot/4
[HttpGet("{radicand}")]
public IActionResult Squareroot(double radicand)
{
if (radicand < 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.NegativeRadicandError
};
HttpContext.Features.Set(errorType);
return Problem(
title: "Bad Input",
detail: "Negative or complex numbers are not valid input.",
type: "https://en.wikipedia.org/wiki/Square_root",
statusCode: StatusCodes.Status400BadRequest
);
}
return Ok(Math.Sqrt(radicand));
}
}
次のアプリを考えてみましょう。
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();
非開発環境では、例外が発生した場合、以下が、クライアントに返される標準の ProblemDetails 応答です。
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}
ほとんどのアプリでは、例外に必要なコードは上記ですべてです。 ただし、次のセクションでは、より詳細な問題の応答を取得する方法を示します。
カスタム例外ハンドラー ページの代わりになるのは、UseExceptionHandler にラムダを提供することです。 ラムダを使用すると、エラーにアクセスし、IProblemDetailsService.WriteAsync
を使用して問題の詳細の応答を書き込むことができます。
using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;
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();
}
else
{
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = Text.Plain;
var title = "Bad Input";
var detail = "Invalid input";
var type = "https://errors.example.com/badInput";
if (context.RequestServices.GetService<IProblemDetailsService>() is
{ } problemDetailsService)
{
var exceptionHandlerFeature =
context.Features.Get<IExceptionHandlerFeature>();
var exceptionType = exceptionHandlerFeature?.Error;
if (exceptionType != null &&
exceptionType.Message.Contains("infinity"))
{
title = "Argument exception";
detail = "Invalid input";
type = "https://errors.example.com/argumentException";
}
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = title,
Detail = detail,
Type = type
}
});
}
});
});
}
app.MapControllers();
app.Run();
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
問題の詳細を生成する別の方法としては、例外とクライアント エラーを問題の詳細にマップするために使用できるサードパーティの NuGet パッケージ Hellang.Middleware.ProblemDetails を使います。
著者: Tom Dykstra
この記事では、ASP.NET Core Web アプリでエラーを処理するための一般的な手法について取り上げます。 「ASP.NET Core コントローラーベースの Web API でのエラー処理」および「Minimal API でのエラー処理」も参照してください。
"開発者例外ページ" には、未処理の要求の例外に関する詳細な情報が表示されます。 次の両方が当てはまる場合、ASP.NET Core アプリでは既定で開発者例外ページが有効になります。
- 開発環境で実行している。
- 現在のテンプレート、つまり、WebApplication.CreateBuilder で作成されたアプリ。
WebHost.CreateDefaultBuilder
で作成されたアプリでは、app.UseDeveloperExceptionPage
でConfigure
を呼び出して開発者例外ページを有効にする必要があります。
開発者例外ページは、後続のミドルウェアでスローされた未処理の例外をキャッチできるように、ミドルウェア パイプラインの早い段階で実行されます。
アプリが運用環境で実行されている場合は、詳細な例外情報を公開しないようにしてください。 環境の構成について詳しくは、「ASP.NET Core で複数の環境を使用する」を参照してください。
開発者例外ページには、例外と要求に関する次の情報が含まれている場合があります。
- スタック トレース
- クエリ文字列のパラメーター (ある場合)
- Cookie (ある場合)
- ヘッダー
開発者例外ページで何らかの情報が提供されるとは限りません。 完全なエラー情報については、ログ記録に関するページを参照してください。
運用環境のカスタム エラー処理ページを構成するには、UseExceptionHandler を呼び出します。 この例外処理ミドルウェアは、次のことを行います。
- 未処理の例外をキャッチしてログに記録します。
- 指定されたパスを使用して、別のパイプラインで要求を再実行します。 応答が始まっていた場合、要求は再実行されません。 テンプレートによって生成されたコードは、
/Error
パスを使用して要求を再実行します。
警告
代替パイプラインで独自の例外がスローされた場合、例外処理ミドルウェアによって元の例外が再スローされます。
このミドルウェアは要求パイプラインを再実行できるため:
- ミドルウェアは、同じ要求での再入を処理する必要があります。 これは通常、
_next
を呼び出した後で状態をクリーンアップすること、またはやり直しを避けるため処理をHttpContext
にキャッシュすることを意味します。 要求本文を処理するときは、これはフォーム リーダーのような結果をバッファーまたはキャッシュすることを意味します。 - テンプレートで使われる UseExceptionHandler(IApplicationBuilder, String) オーバーロードの場合、要求パスのみが変更され、ルート データはクリアされます。 ヘッダー、メソッド、項目などの要求データは、すべてそのまま再利用されます。
- スコープ付きサービスは同じままです。
次の例では、UseExceptionHandler により非開発環境に例外処理ミドルウェアが追加されます。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
Razor Pages アプリのテンプレートには、エラー ページ (.cshtml
) と PageModel クラス (ErrorModel
) が Pages フォルダー内に用意されています。 MVC アプリの場合、プロジェクト テンプレートには、Error
コントローラー用の Home アクション メソッドとエラー ビューが含まれています。
例外処理ミドルウェアでは、"元の" HTTP メソッドを使用して要求が再実行されます。 エラー ハンドラーのエンドポイントが特定の HTTP メソッドのセットに制限されている場合は、それらの HTTP メソッドに対してのみ実行されます。 たとえば、[HttpGet]
属性を使用する MVC コントローラーのアクションは、GET 要求に対してのみ実行されます。 "すべての" 要求がカスタム エラー処理ページに到達するようにするために、それらを特定の HTTP メソッドのセットに制限しないでください。
元の HTTP メソッドに応じて例外を異なる方法で処理するには:
- Razor Pages の場合は、複数のハンドラー メソッドを作成します。 たとえば、GET 例外を処理するために
OnGet
を使用し、POST 例外を処理するためにOnPost
を使用します。 - MVC の場合は、複数のアクションに HTTP 動詞属性を適用します。 たとえば、GET 例外を処理するために
[HttpGet]
を使用し、POST 例外を処理するために[HttpPost]
を使用します。
認証されていないユーザーがカスタム エラー処理ページを表示できるようにするには、匿名アクセスがサポートされるようにします。
エラー ハンドラーで例外や元の要求パスにアクセスするには、IExceptionHandlerPathFeature を使います。 次の例では、IExceptionHandlerPathFeature
を使用して、スローされた例外に関する詳細を取得しています。
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public string? ExceptionMessage { get; set; }
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
ExceptionMessage = "The file was not found.";
}
if (exceptionHandlerPathFeature?.Path == "/")
{
ExceptionMessage ??= string.Empty;
ExceptionMessage += " Page: Home.";
}
}
}
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
カスタム例外ハンドラー ページの代わりになるのは、UseExceptionHandler にラムダを提供することです。 ラムダを使うと、応答を返す前にエラーにアクセスできます。
次のコードは、例外処理にラムダを使用しています。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
// using static System.Net.Mime.MediaTypeNames;
context.Response.ContentType = Text.Plain;
await context.Response.WriteAsync("An exception was thrown.");
var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
await context.Response.WriteAsync(" The file was not found.");
}
if (exceptionHandlerPathFeature?.Path == "/")
{
await context.Response.WriteAsync(" Page: Home.");
}
});
});
app.UseHsts();
}
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
ASP.NET Core アプリでは、既定で、"404 - 見つかりません" などの HTTP エラー状態コードの状態コード ページが表示されません。 アプリで、本文のない HTTP 400 から 599 のエラー状態コードが設定されると、状態コードと空の応答本文が返されます。 一般的なエラー状態コード用に既定のテキスト専用ハンドラーを有効にするには、UseStatusCodePages で Program.cs
を呼び出します。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages();
要求処理ミドルウェアの前に UseStatusCodePages
を呼び出します。 たとえば、静的ファイル ミドルウェアとエンドポイント ミドルウェアの前に UseStatusCodePages
を呼び出します。
UseStatusCodePages
を使用しない場合、エンドポイントなしで URL に移動すると、エンドポイントが見つからないことを示すブラウザー依存のエラー メッセージが返されます。 UseStatusCodePages
が呼び出されると、ブラウザーにより次の応答が返されます。
Status Code: 404; Not Found
UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
注意
状態コード ページのミドルウェアは例外をキャッチしません。 カスタム エラー処理ページを提供するには、例外ハンドラー ページを使用します。
応答の内容の種類とテキストをカスタマイズするには、内容の種類と書式文字列を受け取る UseStatusCodePages のオーバーロードを使います。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");
上記のコードでは、{0}
はエラー コードのプレースホルダーです。
書式指定文字列を含む UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
カスタム エラー処理と応答書き込みコードを指定するには、ラムダ式を受け取る UseStatusCodePages のオーバーロードを使います。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages(async statusCodeContext =>
{
// using static System.Net.Mime.MediaTypeNames;
statusCodeContext.HttpContext.Response.ContentType = Text.Plain;
await statusCodeContext.HttpContext.Response.WriteAsync(
$"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
ラムダを含む UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
UseStatusCodePagesWithRedirects 拡張メソッド:
- クライアントに 302 - Found 状態コードを送信します。
- URL テンプレートで指定されているエラー処理エンドポイントにクライアントをリダイレクトします。 エラー処理エンドポイントには、通常、エラー情報が表示され、HTTP 200 が返されます。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
前のコードに示されているように、URL テンプレートには状態コード用の {0}
プレースホルダーを含めることができます。 URL テンプレートが ~
(チルダ) で始まっている場合、~
はアプリの PathBase
に置き換えられます。 アプリでエンドポイントを指定するときに、そのエンドポイントの MVC ビューまたは Razor ページを作成します。
この方法は、次のようなアプリで一般的に使用されます。
- クライアントを別のエンドポイントにリダイレクトする必要がある場合 (通常は、別のアプリがエラーを処理する場合)。 Web アプリの場合は、クライアントのブラウザーのアドレス バーにリダイレクトされたエンドポイントが反映されます。
- 元の状態コードを保持し、初回のリダイレクト応答で返してはいけない場合。
UseStatusCodePagesWithReExecute 拡張メソッド:
- 代替パスを使用して要求パイプラインを再実行することで、応答本文を生成します。
- パイプラインの再実行前または再実行後に状態コードを変更しません。
新しいパイプラインでは状態コードが完全に制御されるため、新しいパイプラインの実行によって応答の状態コードが変更される可能性があります。 新しいパイプラインで状態コードが変更されない場合、元の状態コードがクライアントに送信されます。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
アプリ内でエンドポイントが指定されている場合は、そのエンドポイントの MVC ビューまたは Razor ページを作成します。
この方法は、次のようなアプリで一般的に使用されます。
- 別のエンドポイントにリダイレクトすることなく要求を処理する。 Web アプリの場合は、クライアントのブラウザーのアドレス バーに、初めに要求されていたエンドポイントが反映されます。
- 元の状態コードを保持し、応答で返す。
URL テンプレートは /
で始まる必要があります。このテンプレートには、状態コード用のプレースホルダー {0}
を含めることができます。 状態コードをクエリ文字列パラメーターとして渡すには、2 番目の引数を UseStatusCodePagesWithReExecute
に渡します。 次に例を示します。
var app = builder.Build();
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");
次の例で示すように、エラーを処理するエンドポイントでは、エラーを生成した元の URL を取得できます。
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
public int OriginalStatusCode { get; set; }
public string? OriginalPathAndQuery { get; set; }
public void OnGet(int statusCode)
{
OriginalStatusCode = statusCode;
var statusCodeReExecuteFeature =
HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
if (statusCodeReExecuteFeature is not null)
{
OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
+ $"{statusCodeReExecuteFeature.OriginalPath}"
+ $"{statusCodeReExecuteFeature.OriginalQueryString}";
}
}
}
このミドルウェアは要求パイプラインを再実行できるため:
- ミドルウェアは、同じ要求での再入を処理する必要があります。 これは通常、
_next
を呼び出した後で状態をクリーンアップすること、またはやり直しを避けるため処理をHttpContext
にキャッシュすることを意味します。 要求本文を処理するときは、これはフォーム リーダーのような結果をバッファーまたはキャッシュすることを意味します。 - スコープ付きサービスは同じままです。
MVC コントローラーまたはアクション メソッドの状態コード ページを無効にするには、[SkipStatusCodePages] 属性を使用します。
Razor Pages ハンドラー メソッドまたは MVC コントローラーの特定の要求に対して状態コード ページを無効にするには、IStatusCodePagesFeature を使用します。
public void OnGet()
{
var statusCodePagesFeature =
HttpContext.Features.Get<IStatusCodePagesFeature>();
if (statusCodePagesFeature is not null)
{
statusCodePagesFeature.Enabled = false;
}
}
例外処理ページのコードが例外をスローすることもあります。 運用環境のエラー ページは十分にテストし、それ自体から例外がスローされないように特に注意する必要があります。
応答のヘッダーが送信された後は、次のようになります。
- アプリで応答の状態コードを変更できません。
- すべての例外ページやハンドラーを実行できません。 応答は完了している必要があります。あるいは、接続が中止となっている必要があります。
アプリ内の例外処理ロジックに加えて、HTTP サーバーの実装でも一部の例外を処理できます。 応答ヘッダーの送信前にサーバーで例外がキャッチされると、サーバーによって "500 - Internal Server Error
" 応答が応答本文なしで送信されます。 応答ヘッダーの送信後にサーバーで例外がキャッチされた場合、サーバーは接続を閉じます。 アプリで処理されない要求はサーバーで処理されます。 サーバーが要求を処理しているときに発生した例外は、すべてサーバーの例外処理によって処理されます。 この動作は、アプリのカスタム エラー ページ、例外処理ミドルウェア、およびフィルターから影響を受けません。
アプリの起動中に起こる例外はホスティング層だけが処理できます。 起動時のエラーをキャプチャしたり、詳細なエラーをキャプチャしたりするように、ホストを構成することができます。
ホスティング レイヤーでは、ホスト アドレス/ポート バインド後にエラーが発生した場合にのみ、キャプチャされた起動時エラーに対するエラー ページを表示できます。 バインドが失敗した場合は、次のようになります。
- ホスティング レイヤーにより重大な例外がログに記録されます。
- dotnet プロセスがクラッシュします。
- HTTP サーバーが Kestrel のときは、エラー ページは表示されません。
IIS (または Azure App Service) または IIS Express 上で実行している場合、プロセスを開始できなければ、ASP.NET Core モジュールから "502.5 - 処理エラー" が返されます。 詳細については、「Azure App Service および IIS での ASP.NET Core のトラブルシューティング」を参照してください。
データベース開発者ページ例外フィルター AddDatabaseDeveloperPageExceptionFilter では、Entity Framework Core の移行を使って解決できるデータベース関連の例外がキャプチャされます。 これらの例外が発生すると、問題が解決する可能性のあるアクションの詳細を含む HTML 応答が生成されます。 このページは、開発環境でのみ有効です。 次のコードでは、データベース開発者ページの例外フィルターを追加しています。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
MVC アプリでは、例外フィルターをグローバルに、またはコントローラーやアクションの単位で構成できます。 Razor Pages アプリでは、グローバルに、またはページ モデルの単位で構成できます。 このようなフィルターは、コントローラー アクションや別のフィルターの実行中に発生する未処理の例外を処理します。 詳細については、「ASP.NET Core フィルター」を参照してください。
例外フィルターは、MVC アクション内で発生する例外をトラップする場合には便利ですが、組み込みの例外処理ミドルウェアUseExceptionHandler ほど柔軟ではありません。 選択された MVC アクションに応じて異なる方法でエラー処理を実行する必要がある場合を除き、UseExceptionHandler
を使用することをお勧めします。
モデル状態エラーを処理する方法については、モデル バインドおよびモデルの検証に関する記事をご覧ください。
問題の詳細は、HTTP API エラーを記述する唯一の応答形式ではありませんが、一般的に HTTP API のエラーを報告するために使用されます。
問題の詳細サービスは、IProblemDetailsService インターフェイスを実装し、これにより、ASP.NET Core での問題の詳細の作成がサポートされます。 AddProblemDetails(IServiceCollection) の IServiceCollection 拡張メソッドは、既定の IProblemDetailsService
実装を登録します。
ASP.NET Core アプリでは、次のミドルウェアによって、AddProblemDetails
が呼び出されたときに問題の詳細 HTTP 応答が生成されます。ただし、Accept
要求 HTTP ヘッダーに、登録された IProblemDetailsWriter (既定値: application/json
) によってサポートされるいずれかのコンテンツ タイプが含まれていない場合を除きます。
- ExceptionHandlerMiddleware: カスタム ハンドラーが定義されていない場合に、問題の詳細の応答を生成します。
- StatusCodePagesMiddleware: 既定で問題の詳細の応答を生成します。
- DeveloperExceptionPageMiddleware: 開発中に
Accept
要求 HTTP ヘッダーにtext/html
が含まれていない場合に、問題の詳細応答を生成します。
次のコードは、"本文コンテンツがまだ含まれていない" すべての HTTP クライアントおよびサーバー エラー応答に対して問題の詳細の応答を生成するようにアプリを構成します。
builder.Services.AddProblemDetails();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
app.UseHsts();
}
app.UseStatusCodePages();
次のセクションでは、問題の詳細の応答の本文をカスタマイズする方法を示します。
ProblemDetails
の自動作成は、次のどのオプションでもカスタマイズできます。
ProblemDetailsOptions.CustomizeProblemDetails
を使用します- カスタム
IProblemDetailsWriter
を使用する - ミドルウェアで
IProblemDetailsService
を呼び出す
生成された問題の詳細は CustomizeProblemDetails を使用してカスタマイズでき、カスタマイズはすべての自動生成された問題の詳細に適用されます。
次のコードは ProblemDetailsOptions を使用して CustomizeProblemDetails を設定します。
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = ctx =>
ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
app.UseHsts();
}
app.UseStatusCodePages();
たとえば、HTTP Status 400 Bad Request
エンドポイントの結果により、次の問題の詳細の応答本文が生成されます。
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Bad Request",
"status": 400,
"nodeId": "my-machine-name"
}
高度なカスタマイズのために IProblemDetailsWriter の実装を作成できます。
public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
// Indicates that only responses with StatusCode == 400
// are handled by this writer. All others are
// handled by different registered writers if available.
public bool CanWrite(ProblemDetailsContext context)
=> context.HttpContext.Response.StatusCode == 400;
public ValueTask WriteAsync(ProblemDetailsContext context)
{
// Additional customizations.
// Write to the response.
var response = context.HttpContext.Response;
return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
}
}
注: カスタムの IProblemDetailsWriter
を使う場合は、IProblemDetailsWriter
、AddRazorPages、AddControllers、または AddControllersWithViews を呼び出す前にカスタムの AddMvc を登録する必要があります。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();
var app = builder.Build();
// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
await next(context);
var mathErrorFeature = context.Features.Get<MathErrorFeature>();
if (mathErrorFeature is not null)
{
if (context.RequestServices.GetService<IProblemDetailsWriter>() is
{ } problemDetailsService)
{
if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
{
(string Detail, string Type) details = mathErrorFeature.MathError switch
{
MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
"https://en.wikipedia.org/wiki/Division_by_zero"),
_ => ("Negative or complex numbers are not valid input.",
"https://en.wikipedia.org/wiki/Square_root")
};
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = "Bad Input",
Detail = details.Detail,
Type = details.Type
}
});
}
}
}
});
// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
if (denominator == 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.DivisionByZeroError
};
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(numerator / denominator);
});
// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
if (radicand < 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.NegativeRadicandError
};
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(Math.Sqrt(radicand));
});
app.Run();
ProblemDetailsOptions を CustomizeProblemDetails と共に使用する別の方法として、ミドルウェアで ProblemDetails を設定します。 問題の詳細の応答は、IProblemDetailsService.WriteAsync
を呼び出すことによって書き込むことができます。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStatusCodePages();
// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
await next(context);
var mathErrorFeature = context.Features.Get<MathErrorFeature>();
if (mathErrorFeature is not null)
{
if (context.RequestServices.GetService<IProblemDetailsService>() is
{ } problemDetailsService)
{
(string Detail, string Type) details = mathErrorFeature.MathError switch
{
MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
"https://en.wikipedia.org/wiki/Division_by_zero"),
_ => ("Negative or complex numbers are not valid input.",
"https://en.wikipedia.org/wiki/Square_root")
};
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = "Bad Input",
Detail = details.Detail,
Type = details.Type
}
});
}
}
});
// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
if (denominator == 0)
{
var errorType = new MathErrorFeature { MathError =
MathErrorType.DivisionByZeroError };
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(numerator / denominator);
});
// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
if (radicand < 0)
{
var errorType = new MathErrorFeature { MathError =
MathErrorType.NegativeRadicandError };
context.Features.Set(errorType);
return Results.BadRequest();
}
return Results.Ok(Math.Sqrt(radicand));
});
app.MapControllers();
app.Run();
前のコードでは、最小限の API エンドポイント /divide
および /squareroot
が、エラー入力時に予期されるカスタム問題の応答を返します。
API コントローラー エンドポイントは、カスタムの問題の応答ではなく、エラー入力で既定の問題の応答を返します。 既定の問題の応答が返されるのは、 が呼び出される前で、応答が再度IProblemDetailsService.WriteAsync
うちに、API コントローラーがエラー状態コードの問題の詳細を応答ストリームに書き込んでいるためです。
次の ValuesController
は BadRequestResult を返します。これは、応答ストリームに書き込むため、カスタムの問題の応答が返されるのを回避します。
[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));
}
}
次の Values3Controller
は ControllerBase.Problem
を返すため、予期されるカスタム問題の結果が返されます。
[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
// /api/values3/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 Problem(
title: "Bad Input",
detail: "Divison by zero is not defined.",
type: "https://en.wikipedia.org/wiki/Division_by_zero",
statusCode: StatusCodes.Status400BadRequest
);
}
return Ok(Numerator / Denominator);
}
// /api/values3/squareroot/4
[HttpGet("{radicand}")]
public IActionResult Squareroot(double radicand)
{
if (radicand < 0)
{
var errorType = new MathErrorFeature
{
MathError = MathErrorType.NegativeRadicandError
};
HttpContext.Features.Set(errorType);
return Problem(
title: "Bad Input",
detail: "Negative or complex numbers are not valid input.",
type: "https://en.wikipedia.org/wiki/Square_root",
statusCode: StatusCodes.Status400BadRequest
);
}
return Ok(Math.Sqrt(radicand));
}
}
次のアプリを考えてみましょう。
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();
非開発環境では、例外が発生した場合、以下が、クライアントに返される標準の ProblemDetails 応答です。
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}
ほとんどのアプリでは、例外に必要なコードは上記ですべてです。 ただし、次のセクションでは、より詳細な問題の応答を取得する方法を示します。
カスタム例外ハンドラー ページの代わりになるのは、UseExceptionHandler にラムダを提供することです。 ラムダを使用すると、エラーにアクセスし、IProblemDetailsService.WriteAsync
を使用して問題の詳細の応答を書き込むことができます。
using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;
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();
}
else
{
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = Text.Plain;
var title = "Bad Input";
var detail = "Invalid input";
var type = "https://errors.example.com/badInput";
if (context.RequestServices.GetService<IProblemDetailsService>() is
{ } problemDetailsService)
{
var exceptionHandlerFeature =
context.Features.Get<IExceptionHandlerFeature>();
var exceptionType = exceptionHandlerFeature?.Error;
if (exceptionType != null &&
exceptionType.Message.Contains("infinity"))
{
title = "Argument exception";
detail = "Invalid input";
type = "https://errors.example.com/argumentException";
}
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = title,
Detail = detail,
Type = type
}
});
}
});
});
}
app.MapControllers();
app.Run();
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
問題の詳細を生成する別の方法としては、例外とクライアント エラーを問題の詳細にマップするために使用できるサードパーティの NuGet パッケージ Hellang.Middleware.ProblemDetails を使います。
著者: Tom Dykstra
この記事では、ASP.NET Core Web アプリでエラーを処理するための一般的な手法について取り上げます。 Web API に関しては、「ASP.NET Core コントローラーベースの Web API でのエラー処理」を参照してください。
"開発者例外ページ" には、未処理の要求の例外に関する詳細な情報が表示されます。 次の両方が当てはまる場合、ASP.NET Core アプリでは既定で開発者例外ページが有効になります。
- 開発環境で実行している。
- 現在のテンプレート、つまり、WebApplication.CreateBuilder で作成されたアプリ。
WebHost.CreateDefaultBuilder
で作成されたアプリでは、app.UseDeveloperExceptionPage
でConfigure
を呼び出して開発者例外ページを有効にする必要があります。
開発者例外ページは、後続のミドルウェアでスローされた未処理の例外をキャッチできるように、ミドルウェア パイプラインの早い段階で実行されます。
アプリが運用環境で実行されている場合は、詳細な例外情報を公開しないようにしてください。 環境の構成について詳しくは、「ASP.NET Core で複数の環境を使用する」を参照してください。
開発者例外ページには、例外と要求に関する次の情報が含まれている場合があります。
- スタック トレース
- クエリ文字列のパラメーター (ある場合)
- Cookie (ある場合)
- ヘッダー
開発者例外ページで何らかの情報が提供されるとは限りません。 完全なエラー情報については、ログ記録に関するページを参照してください。
運用環境のカスタム エラー処理ページを構成するには、UseExceptionHandler を呼び出します。 この例外処理ミドルウェアは、次のことを行います。
- 未処理の例外をキャッチしてログに記録します。
- 指定されたパスを使用して、別のパイプラインで要求を再実行します。 応答が始まっていた場合、要求は再実行されません。 テンプレートによって生成されたコードは、
/Error
パスを使用して要求を再実行します。
警告
代替パイプラインで独自の例外がスローされた場合、例外処理ミドルウェアによって元の例外が再スローされます。
次の例では、UseExceptionHandler により非開発環境に例外処理ミドルウェアが追加されます。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
Razor Pages アプリのテンプレートには、エラー ページ (.cshtml
) と PageModel クラス (ErrorModel
) が Pages フォルダー内に用意されています。 MVC アプリの場合、プロジェクト テンプレートには、Error
コントローラー用の Home アクション メソッドとエラー ビューが含まれています。
例外処理ミドルウェアでは、"元の" HTTP メソッドを使用して要求が再実行されます。 エラー ハンドラーのエンドポイントが特定の HTTP メソッドのセットに制限されている場合は、それらの HTTP メソッドに対してのみ実行されます。 たとえば、[HttpGet]
属性を使用する MVC コントローラーのアクションは、GET 要求に対してのみ実行されます。 "すべての" 要求がカスタム エラー処理ページに到達するようにするために、それらを特定の HTTP メソッドのセットに制限しないでください。
元の HTTP メソッドに応じて例外を異なる方法で処理するには:
- Razor Pages の場合は、複数のハンドラー メソッドを作成します。 たとえば、GET 例外を処理するために
OnGet
を使用し、POST 例外を処理するためにOnPost
を使用します。 - MVC の場合は、複数のアクションに HTTP 動詞属性を適用します。 たとえば、GET 例外を処理するために
[HttpGet]
を使用し、POST 例外を処理するために[HttpPost]
を使用します。
認証されていないユーザーがカスタム エラー処理ページを表示できるようにするには、匿名アクセスがサポートされるようにします。
エラー ハンドラーで例外や元の要求パスにアクセスするには、IExceptionHandlerPathFeature を使います。 次の例では、IExceptionHandlerPathFeature
を使用して、スローされた例外に関する詳細を取得しています。
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public string? ExceptionMessage { get; set; }
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
ExceptionMessage = "The file was not found.";
}
if (exceptionHandlerPathFeature?.Path == "/")
{
ExceptionMessage ??= string.Empty;
ExceptionMessage += " Page: Home.";
}
}
}
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
カスタム例外ハンドラー ページの代わりになるのは、UseExceptionHandler にラムダを提供することです。 ラムダを使うと、応答を返す前にエラーにアクセスできます。
次のコードは、例外処理にラムダを使用しています。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
// using static System.Net.Mime.MediaTypeNames;
context.Response.ContentType = Text.Plain;
await context.Response.WriteAsync("An exception was thrown.");
var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
await context.Response.WriteAsync(" The file was not found.");
}
if (exceptionHandlerPathFeature?.Path == "/")
{
await context.Response.WriteAsync(" Page: Home.");
}
});
});
app.UseHsts();
}
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
ASP.NET Core アプリでは、既定で、"404 - 見つかりません" などの HTTP エラー状態コードの状態コード ページが表示されません。 アプリで、本文のない HTTP 400 から 599 のエラー状態コードが設定されると、状態コードと空の応答本文が返されます。 一般的なエラー状態コード用に既定のテキスト専用ハンドラーを有効にするには、UseStatusCodePages で Program.cs
を呼び出します。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages();
要求処理ミドルウェアの前に UseStatusCodePages
を呼び出します。 たとえば、静的ファイル ミドルウェアとエンドポイント ミドルウェアの前に UseStatusCodePages
を呼び出します。
UseStatusCodePages
を使用しない場合、エンドポイントなしで URL に移動すると、エンドポイントが見つからないことを示すブラウザー依存のエラー メッセージが返されます。 UseStatusCodePages
が呼び出されると、ブラウザーにより次の応答が返されます。
Status Code: 404; Not Found
UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
注意
状態コード ページのミドルウェアは例外をキャッチしません。 カスタム エラー処理ページを提供するには、例外ハンドラー ページを使用します。
応答の内容の種類とテキストをカスタマイズするには、内容の種類と書式文字列を受け取る UseStatusCodePages のオーバーロードを使います。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");
上記のコードでは、{0}
はエラー コードのプレースホルダーです。
書式指定文字列を含む UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
カスタム エラー処理と応答書き込みコードを指定するには、ラムダ式を受け取る UseStatusCodePages のオーバーロードを使います。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages(async statusCodeContext =>
{
// using static System.Net.Mime.MediaTypeNames;
statusCodeContext.HttpContext.Response.ContentType = Text.Plain;
await statusCodeContext.HttpContext.Response.WriteAsync(
$"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
ラムダを含む UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
UseStatusCodePagesWithRedirects 拡張メソッド:
- クライアントに 302 - Found 状態コードを送信します。
- URL テンプレートで指定されているエラー処理エンドポイントにクライアントをリダイレクトします。 エラー処理エンドポイントには、通常、エラー情報が表示され、HTTP 200 が返されます。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
前のコードに示されているように、URL テンプレートには状態コード用の {0}
プレースホルダーを含めることができます。 URL テンプレートが ~
(チルダ) で始まっている場合、~
はアプリの PathBase
に置き換えられます。 アプリでエンドポイントを指定するときに、そのエンドポイントの MVC ビューまたは Razor ページを作成します。
この方法は、次のようなアプリで一般的に使用されます。
- クライアントを別のエンドポイントにリダイレクトする必要がある場合 (通常は、別のアプリがエラーを処理する場合)。 Web アプリの場合は、クライアントのブラウザーのアドレス バーにリダイレクトされたエンドポイントが反映されます。
- 元の状態コードを保持し、初回のリダイレクト応答で返してはいけない場合。
UseStatusCodePagesWithReExecute 拡張メソッド:
- 元の状態コードをクライアントに返します。
- 代替パスを使用して要求パイプラインを再実行することで、応答本文を生成します。
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
アプリ内でエンドポイントが指定されている場合は、そのエンドポイントの MVC ビューまたは Razor ページを作成します。
この方法は、次のようなアプリで一般的に使用されます。
- 別のエンドポイントにリダイレクトすることなく要求を処理する。 Web アプリの場合は、クライアントのブラウザーのアドレス バーに、初めに要求されていたエンドポイントが反映されます。
- 元の状態コードを保持し、応答で返す。
URL テンプレートは /
で始まる必要があります。このテンプレートには、状態コード用のプレースホルダー {0}
を含めることができます。 状態コードをクエリ文字列パラメーターとして渡すには、2 番目の引数を UseStatusCodePagesWithReExecute
に渡します。 次に例を示します。
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");
次の例で示すように、エラーを処理するエンドポイントでは、エラーを生成した元の URL を取得できます。
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
public int OriginalStatusCode { get; set; }
public string? OriginalPathAndQuery { get; set; }
public void OnGet(int statusCode)
{
OriginalStatusCode = statusCode;
var statusCodeReExecuteFeature =
HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
if (statusCodeReExecuteFeature is not null)
{
OriginalPathAndQuery = string.Join(
statusCodeReExecuteFeature.OriginalPathBase,
statusCodeReExecuteFeature.OriginalPath,
statusCodeReExecuteFeature.OriginalQueryString);
}
}
}
MVC コントローラーまたはアクション メソッドの状態コード ページを無効にするには、[SkipStatusCodePages] 属性を使用します。
Razor Pages ハンドラー メソッドまたは MVC コントローラーの特定の要求に対して状態コード ページを無効にするには、IStatusCodePagesFeature を使用します。
public void OnGet()
{
var statusCodePagesFeature =
HttpContext.Features.Get<IStatusCodePagesFeature>();
if (statusCodePagesFeature is not null)
{
statusCodePagesFeature.Enabled = false;
}
}
例外処理ページのコードが例外をスローすることもあります。 運用環境のエラー ページは十分にテストし、それ自体から例外がスローされないように特に注意する必要があります。
応答のヘッダーが送信された後は、次のようになります。
- アプリで応答の状態コードを変更できません。
- すべての例外ページやハンドラーを実行できません。 応答は完了している必要があります。あるいは、接続が中止となっている必要があります。
アプリ内の例外処理ロジックに加えて、HTTP サーバーの実装でも一部の例外を処理できます。 応答ヘッダーの送信前にサーバーで例外がキャッチされると、サーバーによって "500 - Internal Server Error
" 応答が応答本文なしで送信されます。 応答ヘッダーの送信後にサーバーで例外がキャッチされた場合、サーバーは接続を閉じます。 アプリで処理されない要求はサーバーで処理されます。 サーバーが要求を処理しているときに発生した例外は、すべてサーバーの例外処理によって処理されます。 この動作は、アプリのカスタム エラー ページ、例外処理ミドルウェア、およびフィルターから影響を受けません。
アプリの起動中に起こる例外はホスティング層だけが処理できます。 起動時のエラーをキャプチャしたり、詳細なエラーをキャプチャしたりするように、ホストを構成することができます。
ホスティング レイヤーでは、ホスト アドレス/ポート バインド後にエラーが発生した場合にのみ、キャプチャされた起動時エラーに対するエラー ページを表示できます。 バインドが失敗した場合は、次のようになります。
- ホスティング レイヤーにより重大な例外がログに記録されます。
- dotnet プロセスがクラッシュします。
- HTTP サーバーが Kestrel のときは、エラー ページは表示されません。
IIS (または Azure App Service) または IIS Express 上で実行している場合、プロセスを開始できなければ、ASP.NET Core モジュールから "502.5 - 処理エラー" が返されます。 詳細については、「Azure App Service および IIS での ASP.NET Core のトラブルシューティング」を参照してください。
データベース開発者ページ例外フィルター AddDatabaseDeveloperPageExceptionFilter では、Entity Framework Core の移行を使って解決できるデータベース関連の例外がキャプチャされます。 これらの例外が発生すると、問題が解決する可能性のあるアクションの詳細を含む HTML 応答が生成されます。 このページは、開発環境でのみ有効です。 次のコードでは、データベース開発者ページの例外フィルターを追加しています。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
MVC アプリでは、例外フィルターをグローバルに、またはコントローラーやアクションの単位で構成できます。 Razor Pages アプリでは、グローバルに、またはページ モデルの単位で構成できます。 このようなフィルターは、コントローラー アクションや別のフィルターの実行中に発生する未処理の例外を処理します。 詳細については、「ASP.NET Core フィルター」を参照してください。
例外フィルターは、MVC アクション内で発生する例外をトラップする場合には便利ですが、組み込みの例外処理ミドルウェアUseExceptionHandler ほど柔軟ではありません。 選択された MVC アクションに応じて異なる方法でエラー処理を実行する必要がある場合を除き、UseExceptionHandler
を使用することをお勧めします。
作成者: Kirk Larkin、Tom Dykstra、Steve Smith
この記事では、ASP.NET Core Web アプリでエラーを処理するための一般的な手法について取り上げます。 Web API に関しては、「ASP.NET Core コントローラーベースの Web API でのエラー処理」を参照してください。
サンプル コードを表示またはダウンロードします。 (ダウンロード方法。)F12 ブラウザー開発者ツールの [ネットワーク] タブは、サンプル アプリをテストする場合に便利です。
"開発者例外ページ" には、未処理の要求の例外に関する詳細な情報が表示されます。 ASP.NET Core テンプレートにより、次のコードが生成されます。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
前の強調表示されたコードは、アプリが開発環境で実行されている場合に、開発者例外ページを有効にします。
このテンプレートでは、後続のミドルウェアでスローされた未処理の例外をキャッチできるように、ミドルウェア パイプラインの早い段階で UseDeveloperExceptionPage が配置されます。
前のコードでは、アプリが開発環境で実行されている場合に "のみ"、開発者例外ページが有効になります。 アプリが運用環境で実行されている場合は、詳細な例外情報を公開しないようにしてください。 環境の構成について詳しくは、「ASP.NET Core で複数の環境を使用する」を参照してください。
開発者例外ページには、例外と要求に関する次の情報が含まれている場合があります。
- スタック トレース
- クエリ文字列のパラメーター (ある場合)
- Cookie (ある場合)
- ヘッダー
開発者例外ページで何らかの情報が提供されるとは限りません。 完全なエラー情報については、ログ記録に関するページを参照してください。
運用環境のカスタム エラー処理ページを構成するには、UseExceptionHandler を呼び出します。 この例外処理ミドルウェアは、次のことを行います。
- 未処理の例外をキャッチしてログに記録します。
- 指定されたパスを使用して、別のパイプラインで要求を再実行します。 応答が始まっていた場合、要求は再実行されません。 テンプレートによって生成されたコードは、
/Error
パスを使用して要求を再実行します。
警告
代替パイプラインで独自の例外がスローされた場合、例外処理ミドルウェアによって元の例外が再スローされます。
次の例では、UseExceptionHandler により非開発環境に例外処理ミドルウェアが追加されます。
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
Razor Pages アプリのテンプレートには、エラー ページ (.cshtml
) と PageModel クラス (ErrorModel
) が Pages フォルダー内に用意されています。 MVC アプリの場合、プロジェクト テンプレートには、Error
コントローラー用の Home アクション メソッドとエラー ビューが含まれています。
例外処理ミドルウェアでは、"元の" HTTP メソッドを使用して要求が再実行されます。 エラー ハンドラーのエンドポイントが特定の HTTP メソッドのセットに制限されている場合は、それらの HTTP メソッドに対してのみ実行されます。 たとえば、[HttpGet]
属性を使用する MVC コントローラーのアクションは、GET 要求に対してのみ実行されます。 "すべての" 要求がカスタム エラー処理ページに到達するようにするために、それらを特定の HTTP メソッドのセットに制限しないでください。
元の HTTP メソッドに応じて例外を異なる方法で処理するには:
- Razor Pages の場合は、複数のハンドラー メソッドを作成します。 たとえば、GET 例外を処理するために
OnGet
を使用し、POST 例外を処理するためにOnPost
を使用します。 - MVC の場合は、複数のアクションに HTTP 動詞属性を適用します。 たとえば、GET 例外を処理するために
[HttpGet]
を使用し、POST 例外を処理するために[HttpPost]
を使用します。
認証されていないユーザーがカスタム エラー処理ページを表示できるようにするには、匿名アクセスがサポートされるようにします。
エラー ハンドラーで例外や元の要求パスにアクセスするには、IExceptionHandlerPathFeature を使います。 次のコードは、ASP.NET Core テンプレートによって生成される既定の ExceptionMessage
に Pages/Error.cshtml.cs
を追加します。
[ResponseCache(Duration=0, Location=ResponseCacheLocation.None, NoStore=true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public string ExceptionMessage { get; set; }
private readonly ILogger<ErrorModel> _logger;
public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
ExceptionMessage = "File error thrown";
_logger.LogError(ExceptionMessage);
}
if (exceptionHandlerPathFeature?.Path == "/index")
{
ExceptionMessage += " from home page";
}
}
}
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
サンプル アプリで例外をテストするには:
- 環境を運用環境に設定します。
webBuilder.UseStartup<Startup>();
のProgram.cs
からコメントを削除します。- ホーム ページで [Trigger an exception](例外をトリガーする) を選択します。
カスタム例外ハンドラー ページの代わりになるのは、UseExceptionHandler にラムダを提供することです。 ラムダを使うと、応答を返す前にエラーにアクセスできます。
次のコードは、例外処理にラムダを使用しています。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;;
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
await context.Response.WriteAsync("ERROR!<br><br>\r\n");
var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
await context.Response.WriteAsync(
"File error thrown!<br><br>\r\n");
}
await context.Response.WriteAsync(
"<a href=\"/\">Home</a><br>\r\n");
await context.Response.WriteAsync("</body></html>\r\n");
await context.Response.WriteAsync(new string(' ', 512));
});
});
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
警告
または IExceptionHandlerFeature からの機密性の高いエラー情報をクライアントに提供IExceptionHandlerPathFeature。 エラーの提供はセキュリティ上のリスクです。
サンプル アプリで例外処理ラムダをテストするには:
- 環境を運用環境に設定します。
webBuilder.UseStartup<StartupLambda>();
のProgram.cs
からコメントを削除します。- ホーム ページで例外 をトリガーするを選択します。
ASP.NET Core アプリでは、既定で、"404 - 見つかりません" などの HTTP エラー状態コードの状態コード ページが表示されません。 アプリで、本文のない HTTP 400 から 599 のエラー状態コードが設定されると、状態コードと空の応答本文が返されます。 状態コード ページを提供するには、状態コード ページ ミドルウェアを使用します。 一般的なエラー状態コード用に既定のテキスト専用ハンドラーを有効にするには、UseStatusCodePages メソッドで Startup.Configure
を呼び出します。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
要求処理ミドルウェアの前に UseStatusCodePages
を呼び出します。 たとえば、静的ファイル ミドルウェアとエンドポイント ミドルウェアの前に UseStatusCodePages
を呼び出します。
UseStatusCodePages
を使用しない場合、エンドポイントなしで URL に移動すると、エンドポイントが見つからないことを示すブラウザー依存のエラー メッセージが返されます。 たとえば、Home/Privacy2
に移動します。 UseStatusCodePages
が呼び出されると、ブラウザーにより次が返されます。
Status Code: 404; Not Found
UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
- 環境を運用環境に設定します。
webBuilder.UseStartup<StartupUseStatusCodePages>();
のProgram.cs
からコメントを削除します。- ホーム ページにあるリンクを選択します。
注意
状態コード ページのミドルウェアは例外をキャッチしません。 カスタム エラー処理ページを提供するには、例外ハンドラー ページを使用します。
応答の内容の種類とテキストをカスタマイズするには、内容の種類と書式文字列を受け取る UseStatusCodePages のオーバーロードを使います。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages(
"text/plain", "Status code page, status code: {0}");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
上記のコードでは、{0}
はエラー コードのプレースホルダーです。
書式指定文字列を含む UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
UseStatusCodePages
で をテストするには、webBuilder.UseStartup<StartupFormat>();
の Program.cs
からコメントを削除します。
カスタム エラー処理と応答書き込みコードを指定するには、ラムダ式を受け取る UseStatusCodePages のオーバーロードを使います。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePages(async context =>
{
context.HttpContext.Response.ContentType = "text/plain";
await context.HttpContext.Response.WriteAsync(
"Status code page, status code: " +
context.HttpContext.Response.StatusCode);
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
ラムダを含む UseStatusCodePages
は、ユーザーにとって役に立たないメッセージを返すため、通常は、運用環境では使用されません。
UseStatusCodePages
で をテストするには、webBuilder.UseStartup<StartupStatusLambda>();
の Program.cs
からコメントを削除します。
UseStatusCodePagesWithRedirects 拡張メソッド:
- クライアントに 302 - Found 状態コードを送信します。
- URL テンプレートで指定されているエラー処理エンドポイントにクライアントをリダイレクトします。 エラー処理エンドポイントには、通常、エラー情報が表示され、HTTP 200 が返されます。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithRedirects("/MyStatusCode?code={0}");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
前のコードに示されているように、URL テンプレートには状態コード用の {0}
プレースホルダーを含めることができます。 URL テンプレートが ~
(チルダ) で始まっている場合、~
はアプリの PathBase
に置き換えられます。 アプリでエンドポイントを指定するときに、そのエンドポイントの MVC ビューまたは Razor ページを作成します。 Razor Pages の例については、サンプル アプリにある Pages/MyStatusCode.cshtml をご覧ください。
この方法は、次のようなアプリで一般的に使用されます。
- クライアントを別のエンドポイントにリダイレクトする必要がある場合 (通常は、別のアプリがエラーを処理する場合)。 Web アプリの場合は、クライアントのブラウザーのアドレス バーにリダイレクトされたエンドポイントが反映されます。
- 元の状態コードを保持し、初回のリダイレクト応答で返してはいけない場合。
UseStatusCodePages
で をテストするには、webBuilder.UseStartup<StartupSCredirect>();
の Program.cs
からコメントを削除します。
UseStatusCodePagesWithReExecute 拡張メソッド:
- 元の状態コードをクライアントに返します。
- 代替パスを使用して要求パイプラインを再実行することで、応答本文を生成します。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/MyStatusCode2", "?code={0}");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
アプリ内でエンドポイントが指定されている場合は、そのエンドポイントの MVC ビューまたは Razor ページを作成します。 UseStatusCodePagesWithReExecute
の前に UseRouting
が配置されていることを確認して、要求を状態ページに再ルーティングできるようにします。 Razor Pages の例については、サンプル アプリにある Pages/MyStatusCode2.cshtml をご覧ください。
この方法は、次のようなアプリで一般的に使用されます。
- 別のエンドポイントにリダイレクトすることなく要求を処理する。 Web アプリの場合は、クライアントのブラウザーのアドレス バーに、初めに要求されていたエンドポイントが反映されます。
- 元の状態コードを保持し、応答で返す。
URL とクエリ文字列のテンプレートには、状態コード用のプレースホルダー {0}
を含めることができます。 URL テンプレートの先頭には、/
を付ける必要があります。
次の例で示すように、エラーを処理するエンドポイントでは、エラーを生成した元の URL を取得できます。
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class MyStatusCode2Model : PageModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public string ErrorStatusCode { get; set; }
public string OriginalURL { get; set; }
public bool ShowOriginalURL => !string.IsNullOrEmpty(OriginalURL);
public void OnGet(string code)
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
ErrorStatusCode = code;
var statusCodeReExecuteFeature = HttpContext.Features.Get<
IStatusCodeReExecuteFeature>();
if (statusCodeReExecuteFeature != null)
{
OriginalURL =
statusCodeReExecuteFeature.OriginalPathBase
+ statusCodeReExecuteFeature.OriginalPath
+ statusCodeReExecuteFeature.OriginalQueryString;
}
}
}
Razor Pages の例については、サンプル アプリにある Pages/MyStatusCode2.cshtml をご覧ください。
UseStatusCodePages
で をテストするには、webBuilder.UseStartup<StartupSCreX>();
の Program.cs
からコメントを削除します。
MVC コントローラーまたはアクション メソッドの状態コード ページを無効にするには、[SkipStatusCodePages] 属性を使用します。
Razor Pages ハンドラー メソッドまたは MVC コントローラーの特定の要求に対して状態コード ページを無効にするには、IStatusCodePagesFeature を使用します。
public void OnGet()
{
// using Microsoft.AspNetCore.Diagnostics;
var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();
if (statusCodePagesFeature != null)
{
statusCodePagesFeature.Enabled = false;
}
}
例外処理ページのコードが例外をスローすることもあります。 運用環境のエラー ページは十分にテストし、それ自体から例外がスローされないように特に注意する必要があります。
応答のヘッダーが送信された後は、次のようになります。
- アプリで応答の状態コードを変更できません。
- すべての例外ページやハンドラーを実行できません。 応答は完了している必要があります。あるいは、接続が中止となっている必要があります。
アプリ内の例外処理ロジックに加えて、HTTP サーバーの実装でも一部の例外を処理できます。 応答ヘッダーの送信前にサーバーで例外がキャッチされると、サーバーによって "500 - Internal Server Error
" 応答が応答本文なしで送信されます。 応答ヘッダーの送信後にサーバーで例外がキャッチされた場合、サーバーは接続を閉じます。 アプリで処理されない要求はサーバーで処理されます。 サーバーが要求を処理しているときに発生した例外は、すべてサーバーの例外処理によって処理されます。 この動作は、アプリのカスタム エラー ページ、例外処理ミドルウェア、およびフィルターから影響を受けません。
アプリの起動中に起こる例外はホスティング層だけが処理できます。 起動時のエラーをキャプチャしたり、詳細なエラーをキャプチャしたりするように、ホストを構成することができます。
ホスティング レイヤーでは、ホスト アドレス/ポート バインド後にエラーが発生した場合にのみ、キャプチャされた起動時エラーに対するエラー ページを表示できます。 バインドが失敗した場合は、次のようになります。
- ホスティング レイヤーにより重大な例外がログに記録されます。
- dotnet プロセスがクラッシュします。
- HTTP サーバーが Kestrel のときは、エラー ページは表示されません。
IIS (または Azure App Service) または IIS Express 上で実行している場合、プロセスを開始できなければ、ASP.NET Core モジュールから "502.5 - 処理エラー" が返されます。 詳細については、「Azure App Service および IIS での ASP.NET Core のトラブルシューティング」を参照してください。
データベース開発者ページ例外フィルター AddDatabaseDeveloperPageExceptionFilter
では、Entity Framework Core の移行を使って解決できるデータベース関連の例外がキャプチャされます。 これらの例外が発生すると、問題が解決する可能性のあるアクションの詳細を含む HTML 応答が生成されます。 このページは、開発環境でのみ有効です。 次のコードは、個々のユーザー アカウントが指定されたときに、ASP.NET Core Razor Pages テンプレートによって生成されたものです。
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
}
MVC アプリでは、例外フィルターをグローバルに、またはコントローラーやアクションの単位で構成できます。 Razor Pages アプリでは、グローバルに、またはページ モデルの単位で構成できます。 このようなフィルターは、コントローラー アクションや別のフィルターの実行中に発生する未処理の例外を処理します。 詳細については、「ASP.NET Core フィルター」を参照してください。
例外フィルターは、MVC アクション内で発生する例外をトラップする場合には便利ですが、組み込みの例外処理ミドルウェアUseExceptionHandler
ほど柔軟ではありません。 選択された MVC アクションに応じて異なる方法でエラー処理を実行する必要がある場合を除き、UseExceptionHandler
を使用することをお勧めします。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
作成者: Tom Dykstra、Steve Smith
この記事では、ASP.NET Core Web アプリでエラーを処理するための一般的な手法について取り上げます。 Web API に関しては、「ASP.NET Core コントローラーベースの Web API でのエラー処理」を参照してください。
サンプル コードを表示またはダウンロードします。 (ダウンロード方法。)
"開発者例外ページ" には、要求の例外に関する詳細な情報が表示されます。 ASP.NET Core テンプレートにより、次のコードが生成されます。
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
前のコードは、アプリが開発環境で実行されている場合に、開発者例外ページを有効にします。
このテンプレートでは、後続のミドルウェアで例外をキャッチできるように、ミドルウェアの前に UseDeveloperExceptionPage が配置されます。
前のコードは、アプリが開発環境で実行されている場合にのみ、開発者例外ページを有効にします。 アプリが運用環境で実行されている場合は、詳細な例外情報を公開しないようにしてください。 環境の構成について詳しくは、「ASP.NET Core で複数の環境を使用する」を参照してください。
開発者例外ページには、例外と要求に関する次の情報が含まれています。
- スタック トレース
- クエリ文字列のパラメーター (ある場合)
- Cookie (ある場合)
- ヘッダー
運用環境用のカスタム エラー処理ページを構成するには、例外処理ミドルウェアを使用します。 ミドルウェアでは次を行います。
- 例外をキャッチしてログに記録します。
- ページ用の、またはコントローラーが指定した別のパイプラインで、要求を再実行します。 応答が始まっていた場合、要求は再実行されません。 テンプレートによって生成されたコードは、
/Error
への要求を再実行します。
次の例では、UseExceptionHandler により非開発環境に例外処理ミドルウェアが追加されます。
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
Razor Pages アプリのテンプレートには、エラー ページ (.cshtml
) と PageModel クラス (ErrorModel
) が Pages フォルダー内に用意されています。 MVC アプリの場合、プロジェクト テンプレートには、Home コントローラーのエラー アクション メソッドとエラー ビューが含まれています。
HttpGet
などの HTTP メソッド属性を使ってエラー ハンドラー アクション メソッドをマークしないでください。 明示的な動詞を使用すると、要求がメソッドに届かないことがあります。 認証されていないユーザーにエラー ビューが表示される場合は、メソッドへの匿名アクセスを許可します。
エラー ハンドラー コントローラーまたはページ内で例外や元の要求パスにアクセスするには、IExceptionHandlerPathFeature を使います。
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public string ExceptionMessage { get; set; }
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
ExceptionMessage = "File error thrown";
}
if (exceptionHandlerPathFeature?.Path == "/index")
{
ExceptionMessage += " from home page";
}
}
}
警告
機密性の高いエラー情報をクライアントに提供しないでください。 エラーの提供はセキュリティ上のリスクです。
上記の例外処理ページをトリガーするには、環境を運用に設定し、例外を強制します。
カスタム例外ハンドラー ページの代わりになるのは、UseExceptionHandler にラムダを提供することです。 ラムダを使うと、応答を返す前にエラーにアクセスできます。
例外処理にラムダを使う例を次に示します。
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
await context.Response.WriteAsync("ERROR!<br><br>\r\n");
var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
await context.Response.WriteAsync("File error thrown!<br><br>\r\n");
}
await context.Response.WriteAsync("<a href=\"/\">Home</a><br>\r\n");
await context.Response.WriteAsync("</body></html>\r\n");
await context.Response.WriteAsync(new string(' ', 512)); // IE padding
});
});
app.UseHsts();
}
上記のコードでは await context.Response.WriteAsync(new string(' ', 512));
が追加されるため、Internet Explorer ブラウザーには IE のエラー メッセージではなく、このエラー メッセージが表示されます。 詳細については、次を参照してください。この GitHub の問題します。
警告
または IExceptionHandlerFeature からの機密性の高いエラー情報をクライアントに提供IExceptionHandlerPathFeature。 エラーの提供はセキュリティ上のリスクです。
ASP.NET Core アプリでは、既定で、"404 - 見つかりません" などの HTTP 状態コードの状態コード ページが表示されません。 アプリでは、状態コードと空の応答本文が返されます。 状態コード ページを提供するには、状態コード ページ ミドルウェアを使用します。
そのミドルウェアは、Microsoft.AspNetCore.Diagnostics パッケージによって使用可能になります。
一般的なエラー状態コード用に既定のテキスト専用ハンドラーを有効にするには、UseStatusCodePages メソッドで Startup.Configure
を呼び出します。
app.UseStatusCodePages();
要求処理ミドルウェア (たとえば、静的ファイル ミドルウェアや MVC ミドルウェア) の前に UseStatusCodePages
を呼び出します。
UseStatusCodePages
を使用しない場合、エンドポイントなしで URL に移動すると、エンドポイントが見つからないことを示すブラウザー依存のエラー メッセージが返されます。 たとえば、Home/Privacy2
に移動します。 UseStatusCodePages
が呼び出されると、ブラウザーにより次が返されます。
Status Code: 404; Not Found
応答の内容の種類とテキストをカスタマイズするには、内容の種類と書式文字列を受け取る UseStatusCodePages のオーバーロードを使います。
app.UseStatusCodePages(
"text/plain", "Status code page, status code: {0}");
カスタム エラー処理と応答書き込みコードを指定するには、ラムダ式を受け取る UseStatusCodePages のオーバーロードを使います。
app.UseStatusCodePages(async context =>
{
context.HttpContext.Response.ContentType = "text/plain";
await context.HttpContext.Response.WriteAsync(
"Status code page, status code: " +
context.HttpContext.Response.StatusCode);
});
UseStatusCodePagesWithRedirects 拡張メソッド:
- クライアントに 302 - Found 状態コードを送信します。
- URL テンプレートで指定された場所にクライアントをリダイレクトします。
app.UseStatusCodePagesWithRedirects("/StatusCode?code={0}");
次の例で示すように、URL テンプレートには状態コード用の {0}
プレースホルダーを含めることができます。 URL テンプレートが ~
(チルダ) で始まっている場合、~
はアプリの PathBase
に置き換えられます。 アプリ内でエンドポイントを指し示す場合は、そのエンドポイントの MVC ビューまたは Razor ページを作成します。 Razor Pages の例については、Pages/StatusCode.cshtml
にある をご覧ください。
この方法は、次のようなアプリで一般的に使用されます。
- クライアントを別のエンドポイントにリダイレクトする必要がある場合 (通常は、別のアプリがエラーを処理する場合)。 Web アプリの場合は、クライアントのブラウザーのアドレス バーにリダイレクトされたエンドポイントが反映されます。
- 元の状態コードを保持し、初回のリダイレクト応答で返してはいけない場合。
UseStatusCodePagesWithReExecute 拡張メソッド:
- 元の状態コードをクライアントに返します。
- 代替パスを使用して要求パイプラインを再実行することで、応答本文を生成します。
app.UseStatusCodePagesWithReExecute("/StatusCode","?code={0}");
アプリ内でエンドポイントを指し示す場合は、そのエンドポイントの MVC ビューまたは Razor ページを作成します。 UseStatusCodePagesWithReExecute
の前に UseRouting
が配置されていることを確認して、要求を状態ページに再ルーティングできるようにします。 Razor Pages の例については、Pages/StatusCode.cshtml
にある をご覧ください。
この方法は、次のようなアプリで一般的に使用されます。
- 別のエンドポイントにリダイレクトすることなく要求を処理する。 Web アプリの場合は、クライアントのブラウザーのアドレス バーに、初めに要求されていたエンドポイントが反映されます。
- 元の状態コードを保持し、応答で返す。
URL とクエリ文字列のテンプレートには、状態コード用のプレースホルダー ({0}
) を含めることができます。 URL テンプレートの先頭には、スラッシュ (/
) を付ける必要があります。 パスでプレースホルダーを使う場合は、エンドポイント (ページまたはコントローラー) でパスのセグメントを処理できることを確認します。 たとえば、エラー用の Razor ページでは、@page
ディレクティブの付いた省略可能なパスのセグメント値を受け入れる必要があります。
@page "{code?}"
次の例で示すように、エラーを処理するエンドポイントでは、エラーを生成した元の URL を取得できます。
var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
if (statusCodeReExecuteFeature != null)
{
OriginalURL =
statusCodeReExecuteFeature.OriginalPathBase
+ statusCodeReExecuteFeature.OriginalPath
+ statusCodeReExecuteFeature.OriginalQueryString;
}
HttpGet
などの HTTP メソッド属性を使ってエラー ハンドラー アクション メソッドをマークしないでください。 明示的な動詞を使用すると、要求がメソッドに届かないことがあります。 認証されていないユーザーにエラー ビューが表示される場合は、メソッドへの匿名アクセスを許可します。
MVC コントローラーまたはアクション メソッドの状態コード ページを無効にするには、[SkipStatusCodePages]
属性を使用します。
Razor Pages ハンドラー メソッドまたは MVC コントローラーの特定の要求に対して状態コード ページを無効にするには、IStatusCodePagesFeature を使用します。
var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();
if (statusCodePagesFeature != null)
{
statusCodePagesFeature.Enabled = false;
}
例外処理ページのコードは例外をスローすることがあります。 実稼働のエラー ページは純粋に静的なコンテンツで構成することをお勧めします。
応答のヘッダーが送信された後は、次のようになります。
- アプリで応答の状態コードを変更できません。
- すべての例外ページやハンドラーを実行できません。 応答は完了している必要があります。あるいは、接続が中止となっている必要があります。
アプリ内の例外処理ロジックに加えて、HTTP サーバーの実装でも一部の例外を処理できます。 応答ヘッダーの送信前にサーバーで例外がキャッチされると、サーバーによって "500 - 内部サーバー エラーです" 応答が応答本文なしで送信されます。 応答ヘッダーの送信後にサーバーで例外がキャッチされた場合、サーバーは接続を閉じます。 アプリで処理されない要求はサーバーで処理されます。 サーバーが要求を処理しているときに発生した例外は、すべてサーバーの例外処理によって処理されます。 この動作は、アプリのカスタム エラー ページ、例外処理ミドルウェア、およびフィルターから影響を受けません。
アプリの起動中に起こる例外はホスティング層だけが処理できます。 起動時のエラーをキャプチャしたり、詳細なエラーをキャプチャしたりするように、ホストを構成することができます。
ホスティング レイヤーでは、ホスト アドレス/ポート バインド後にエラーが発生した場合にのみ、キャプチャされた起動時エラーに対するエラー ページを表示できます。 バインドが失敗した場合は、次のようになります。
- ホスティング レイヤーにより重大な例外がログに記録されます。
- dotnet プロセスがクラッシュします。
- HTTP サーバーが Kestrel のときは、エラー ページは表示されません。
IIS (または Azure App Service) または IIS Express 上で実行している場合、プロセスを開始できなければ、ASP.NET Core モジュールから "502.5 - 処理エラー" が返されます。 詳細については、「Azure App Service および IIS での ASP.NET Core のトラブルシューティング」を参照してください。
データベース エラー ページ ミドルウェアでは、Entity Framework の移行を使って解決できるデータベース関連の例外がキャプチャされます。 これらの例外が発生すると、問題が解決する可能性のあるアクションの詳細を含む HTML 応答が生成されます。 このページは、開発環境でのみ有効にする必要があります。 ページを有効にするには、コードを Startup.Configure
に追加します。
if (env.IsDevelopment())
{
app.UseDatabaseErrorPage();
}
UseDatabaseErrorPage には、Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet パッケージが必要です。
MVC アプリでは、例外フィルターをグローバルに、またはコントローラーやアクションの単位で構成できます。 Razor Pages アプリでは、グローバルに、またはページ モデルの単位で構成できます。 このようなフィルターはコントローラー アクションや別のフィルターの実行中に発生する未処理の例外を処理します。 詳細については、「ASP.NET Core フィルター」を参照してください。
ヒント
例外フィルターは、MVC アクション内で発生した例外をトラップする場合は便利ですが、例外処理ミドルウェアほど柔軟ではありません。 ミドルウェアの使用をお勧めします。 選択された MVC アクションに応じて異なる方法でエラー処理を実行する必要がある場合にのみ、フィルターを使用します。
ASP.NET Core に関するフィードバック
ASP.NET Core はオープンソース プロジェクトです。 フィードバックを提供するにはリンクを選択します。
その他のリソース
トレーニング
モジュール
ミドルウェアを使用して ASP.NET Core の動作をカスタマイズする - Training
ASP.NET Core アプリのミドルウェアを理解して実装します。 HTTP ログや認証などの付属ミドルウェアを使用する。 要求と応答を処理するカスタム ミドルウェアを作成する。