こんにちは、MLBお兄さんこと松村です。
大谷翔平選手が 50-50 (50本塁打-50盗塁) を達成しましたね。と思っていたら 55-55 の可能性がもう目前まで迫っているという、素晴らしすぎるシーズンになっていますね。
.NET 7 がリリースされてから、ASP.NET Core Web API のドキュメントやサンプルコードに、このコードが記載されるようになっていたことに気付きました。
var builder = WebApplication.CreateBuilder(args); builder.Services.AddProblemDetails(); var app = builder.Build(); app.MapGet("/", () => "Hello World"); app.Run();
builder.Services.AddProblemDetails();
で使用されている IServiceCollection.AddProblemDetails()
について調べてみました。
IServiceCollection.AddProblemDetails()
IServiceCollection.AddProblemDetails()
はミドルウェアとして利用することができます。
このミドルウェアは .NET 7 から追加されていたようです。ドキュメントはこちらです。
ドキュメントでは以下のように言及されています。
問題の詳細は、HTTP API エラーを記述する唯一の応答形式ではありませんが、一般的に HTTP API のエラーを報告するために使用されます。
問題の詳細サービスは、IProblemDetailsService インターフェイスを実装し、これにより、ASP.NET Core での問題の詳細の作成がサポートされます。 IServiceCollection の AddProblemDetails(IServiceCollection) 拡張メソッドは、既定の IProblemDetailsService 実装を登録します。
この「問題の詳細」というのは、RFC 7807 の「Problem Details for HTTP APIs」を指しています。
この RFC は Web API におけるエラー情報を表現するための、統一的なレスポンス形式を説明しています。
www.rfc-editor.org
ざっと RFC 7807 を読みましたが、下記の構成で問題の詳細を表現するようです。
- ヘッダー
Content-Type
はapplication/problem+json
またはapplication/problem+xml
とする
- ボディ
"type"
: 問題の種類に該当する RFC の URI ※必須"title"
: 問題の要約 (文字列)"status"
: HTTP ステータスコード (数値)"detail"
: 問題の詳細"instance"
: 問題の特定の発生を識別する URI- 上記以外の項目も追加可能 (拡張)
エラーレスポンスを確認してみる
それでは実際に RFC 7807 の形式のエラーレスポンスを確認してみます。
サンプルとしてエンドポイントを持たない Minimal API を使用します。
var builder = WebApplication.CreateBuilder(args); builder.Services.AddProblemDetails(); var app = builder.Build(); app.UseStatusCodePages(); app.Run();
存在しないエンドポイントに HTTP GET リクエストを送信し、レスポンスを確認します。
curl -i http://localhost:5236/users HTTP/1.1 404 Not Found Content-Type: application/problem+json Date: Sun, 22 Sep 2024 12:28:43 GMT Server: Kestrel Transfer-Encoding: chunked {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404}
"type"
, "title"
, "status"
が既定で含まれることが確認できました。
IServiceCollection.AddProblemDetails()
で登録されたミドルウェアによって、HTTP 404 のレスポンスが生成されています。
レスポンスの項目を拡張する
IServiceCollection.AddProblemDetails()
のオプションを構成することで、問題の詳細のレスポンスに任意の項目を追加することができます。
下記の例では、現在時刻、URL のパス、URL のクエリ文字列を含めるようにしています。
builder.Services.AddProblemDetails(options => options.CustomizeProblemDetails = ctx => { ctx.ProblemDetails.Extensions.Add("now", DateTimeOffset.Now); ctx.ProblemDetails.Extensions.Add("path", ctx.HttpContext.Request.Path.ToString()); ctx.ProblemDetails.Extensions.Add("query", ctx.HttpContext.Request.Query); });
curl -i http://localhost:5236/users/1?name=yuta HTTP/1.1 404 Not Found Content-Type: application/problem+json Date: Sun, 22 Sep 2024 13:08:56 GMT Server: Kestrel Transfer-Encoding: chunked {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404,"now":"2024-09-22T22:08:57.2497121+09:00","path":"/users/1","query":[{"key":"name","value":["yuta"]}]}
例外発生時のエラーレスポンスを構成する
var builder = WebApplication.CreateBuilder(args); builder.Services.AddProblemDetails(); var app = builder.Build(); app.UseStatusCodePages(); app.MapGet("/exception", () => { throw new InvalidOperationException("This is an exception"); }); app.Run();
curl -i http://localhost:5236/exception HTTP/1.1 500 Internal Server Error Content-Type: application/problem+json Date: Sun, 22 Sep 2024 12:56:43 GMT Server: Kestrel Transfer-Encoding: chunked {"type":"https://tools.ietf.org/html/rfc9110#section-15.6.1","title":"System.InvalidOperationException","status":500,"detail":"This is an exception","exception":{"details":"System.InvalidOperationException: This is an exception\r\n at Program.<>c.<<Main>$>b__0_0() in D:\\source\\tmp\\dotnet\\web\\ProblemDetailsApp\\Net8ProblemDetailsApp\\Program.cs:line 84\r\n at lambda_method1(Closure, Object, HttpContext)\r\n at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)\r\n at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)","headers":{"Accept":["*/*"],"Host":["localhost:5236"],"User-Agent":["curl/8.8.0"]},"path":"/exception","endpoint":"HTTP: GET /exception","routeValues":{}}}
ヘッダーやボディ構成は同様ですが、例外メッセージが "detail"
に、例外情報が "exception"
に含まれるようになりました。
開発環境 (ASPNETCORE_ENVIRONMENT=Development) で動かしているため、開発者例外ページの内容が含まれています。
なお、本番環境 (ASPNETCORE_ENVIRONMENT=Production) の場合はレスポンスボディが無い状態となります。
curl -i http://localhost:5236/exception HTTP/1.1 500 Internal Server Error Content-Length: 0 Date: Sun, 22 Sep 2024 13:45:39 GMT Server: Kestrel
そのため例外ハンドラーを使用して、HTTP 500 のレスポンスを構成します。
app.UseExceptionHandler(exceptionHandlerApp => exceptionHandlerApp.Run(async context => await Results.Problem().ExecuteAsync(context))); app.MapGet("/exception", () => { throw new InvalidOperationException("This is an exception"); }); app.Run();
curl -i http://localhost:5236/exception HTTP/1.1 500 Internal Server Error Content-Type: application/problem+json Date: Sun, 22 Sep 2024 13:18:22 GMT Server: Kestrel Cache-Control: no-cache,no-store Expires: -1 Pragma: no-cache Transfer-Encoding: chunked {"type":"https://tools.ietf.org/html/rfc9110#section-15.6.1","title":"An error occurred while processing your request.","status":500}
Web API のエラーハンドリングはミドルウェアで自作することが多いと思いますが、RFC に則ったフォーマットにしておけばクライアント側も振る舞いを合わせやすいので、積極的に使っていきたい機能です。
サービス一覧 www.alterbooth.com cloudpointer.tech www.alterbooth.com