using System.Net; using IdokladSdk.Exceptions; using Microsoft.AspNetCore.Mvc; using Idoklad.Client; using Idoklad.Credentials; namespace Idoklad.Infrastructure; /// /// Translates domain exceptions into JSON responses: /// missing credentials become 401, and upstream iDoklad failures surface the upstream status. /// public sealed class ExceptionHandlingMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { try { await _next(context); } catch (MissingCredentialsException ex) { await WriteProblem(context, HttpStatusCode.Unauthorized, ex.Message, new Dictionary { ["missingHeaders"] = ex.MissingHeaders, }); } catch (IdokladApiException ex) { // Never log decoded credentials; only the upstream status and message. _logger.LogWarning("iDoklad API call failed: {Status} {ErrorCode} {Message}", ex.StatusCode, ex.ErrorCode, ex.Message); await WriteProblem(context, ex.StatusCode, ex.Message, new Dictionary { ["idokladErrorCode"] = ex.ErrorCode.ToString(), }); } catch (IdokladAuthenticationException ex) { // OAuth2 token acquisition failed (bad client id/secret/application id). _logger.LogWarning("iDoklad authentication failed: {Error}", ex.AuthenticationError?.Error ?? ex.Message); await WriteProblem(context, HttpStatusCode.Unauthorized, ex.AuthenticationError?.ErrorDescription ?? ex.Message, new Dictionary { ["idokladError"] = ex.AuthenticationError?.Error, }); } catch (IdokladBaseException ex) { // Other SDK-level failures (malformed responses, batch errors, etc.). _logger.LogWarning("iDoklad SDK error: {Message}", ex.Message); await WriteProblem(context, HttpStatusCode.BadGateway, ex.Message, null); } catch (HttpRequestException ex) { // The iDoklad API / identity server is unreachable. _logger.LogWarning(ex, "iDoklad API unreachable."); await WriteProblem(context, HttpStatusCode.BadGateway, "iDoklad API is unreachable.", null); } catch (ArgumentException ex) { // Thrown by the SDK when required credential fields are blank/invalid. await WriteProblem(context, HttpStatusCode.BadRequest, ex.Message, null); } } private static async Task WriteProblem(HttpContext context, HttpStatusCode status, string detail, IDictionary? extensions) { if (context.Response.HasStarted) { return; } var problem = new ProblemDetails { Status = (int)status, Title = ReasonPhrase(status), Detail = detail, }; if (extensions is not null) { foreach (var (key, value) in extensions) { problem.Extensions[key] = value; } } context.Response.Clear(); context.Response.StatusCode = (int)status; context.Response.ContentType = "application/problem+json"; await context.Response.WriteAsJsonAsync(problem); } private static string ReasonPhrase(HttpStatusCode status) => status switch { HttpStatusCode.Unauthorized => "Unauthorized", HttpStatusCode.BadRequest => "Bad Request", HttpStatusCode.BadGateway => "Upstream iDoklad API error", _ => status.ToString(), }; }