バックエンドエラーハンドリング
例外ハンドリングミドルウェア
すべての未ハンドル例外を捕捉し、RFC 7807 Problem Details にマッピングするグローバル例外ハンドラー:
csharp
public class GlobalExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
public GlobalExceptionHandlerMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionHandlerMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var traceId = Activity.Current?.TraceId.ToString()
?? context.TraceIdentifier;
var tenantId = context.User.FindFirstValue("tenant_id");
var userId = context.User.FindFirstValue("sub");
// 構造化ログ出力(機密情報を含めない)
_logger.LogError(
exception,
"Unhandled exception occurred. " +
"TraceId: {TraceId}, TenantId: {TenantId}, UserId: {UserId}, " +
"Path: {Path}, Method: {Method}",
traceId, tenantId, userId,
context.Request.Path, context.Request.Method);
var problemDetails = exception switch
{
ValidationException validationEx => CreateValidationProblem(
context, traceId, validationEx),
BusinessRuleException businessEx => CreateBusinessProblem(
context, traceId, businessEx),
UnauthorizedAccessException => CreateAuthProblem(
context, traceId),
ConcurrencyException concurrencyEx => CreateConcurrencyProblem(
context, traceId, concurrencyEx),
_ => CreateSystemProblem(context, traceId),
};
context.Response.StatusCode = problemDetails.Status ?? 500;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(problemDetails);
}
private static ProblemDetails CreateSystemProblem(
HttpContext context, string traceId)
{
// システムエラーでは内部詳細を公開しない
return new ProblemDetails
{
Type = "https://tashika.example/errors/system/internal",
Title = "Internal Server Error",
Status = StatusCodes.Status500InternalServerError,
Detail = "システムエラーが発生しました。しばらく時間をおいてから再度お試しください。",
Instance = context.Request.Path,
Extensions =
{
["traceId"] = traceId,
["errorCode"] = "TSHK-SYS-001",
},
};
}
// 他の Create*Problem メソッドも同様のパターンで実装
}ミドルウェア登録順序:
csharp
app.UseMiddleware<GlobalExceptionHandlerMiddleware>(); // 最外周
app.UseMiddleware<TraceIdResponseMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.MapEndpoints();FluentValidation エラーマッピング
FluentValidation のバリデーションエラーを RFC 7807 Problem Details の errors フィールドにマッピングする:
csharp
public class ValidationExceptionHandler
{
public static ProblemDetails ToProblemDetails(
ValidationException exception,
string traceId,
string instance)
{
var errors = exception.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(
g => ToCamelCase(g.Key),
g => g.Select(e => e.ErrorMessage).ToArray());
return new ProblemDetails
{
Type = "https://tashika.example/errors/validation",
Title = "Validation Error",
Status = StatusCodes.Status400BadRequest,
Detail = "入力内容に誤りがあります。修正してください。",
Instance = instance,
Extensions =
{
["traceId"] = traceId,
["errorCode"] = "TSHK-VAL-001",
["errors"] = errors,
},
};
}
}外部サービス障害時の縮退運転
→ 関連: AV-003 弾力性
外部依存が障害を起こした場合のフォールバック動作を定義する:
| 外部サービス | Circuit Breaker | フォールバック動作 | ユーザーへの影響 |
|---|---|---|---|
| マイナポータル API | あり | 手動 XML アップロードモードに切り替え | 「自動取込が一時的に利用できません。XMLファイルを手動でアップロードしてください」 |
| e-Tax / eLTAX | あり | ファイル出力(手動提出)モードに切り替え。送信データをキューに保持し復旧後に自動再送 | 「電子申告は現在キューに入っています。復旧後に自動送信されます」 |
| SendGrid(メール) | あり | メール送信をキューに保持しリトライ。一定期間後はシステム内通知に切り替え | メール通知が遅延。システム内通知は即時表示 |
| Cloud KMS | なし(必須依存) | サービス利用不可。暗号化/復号を要する操作をすべてブロック | 「セキュリティサービスに接続できません。しばらくお待ちください」 |
| Cloud Storage | なし(必須依存) | ファイルアップロード/ダウンロードをブロック | 「ファイル操作が一時的に利用できません」 |
Tax Calculation Core エラー
税計算コアは純粋関数ライブラリとして設計されており、外部状態に起因する例外は発生しない。以下のドメイン固有エラーのみを定義する:
| エラー種別 | クラス名 | 説明 | 対応 |
|---|---|---|---|
| 入力バリデーション | TaxCalculationValidationError | 不正な入力値(負の所得額、不正な年齢等) | フィールドレベルエラーとして返却 |
| 精度オーバーフロー | TaxCalculationOverflowError | decimal の精度を超える計算結果 | TSHK-SYS-004 として記録。サポートへの問い合わせを促す |
| 年度パラメータ未設定 | TaxYearNotConfiguredError | 対象年度の税率テーブル・控除パラメータが未登録 | TSHK-BIZ-003 として返却。管理者にマスタ設定を促す |
csharp
// Tax Calculation Core のエラー型(Domain 層に定義)
public sealed record TaxCalculationValidationError(
string FieldName,
string Message);
public sealed record TaxCalculationResult
{
// 正常結果
public required decimal AnnualTaxAmount { get; init; }
// ... 他のフィールド
// バリデーションエラー(計算実行前に検出)
public IReadOnlyList<TaxCalculationValidationError> ValidationErrors { get; init; }
= Array.Empty<TaxCalculationValidationError>();
public bool IsValid => ValidationErrors.Count == 0;
}