Skip to content

バックエンドエラーハンドリング

例外ハンドリングミドルウェア

すべての未ハンドル例外を捕捉し、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不正な入力値(負の所得額、不正な年齢等)フィールドレベルエラーとして返却
精度オーバーフローTaxCalculationOverflowErrordecimal の精度を超える計算結果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;
}