diff --git a/Program.cs b/Program.cs index 3408b45..aa0469d 100644 --- a/Program.cs +++ b/Program.cs @@ -129,6 +129,11 @@ app.MapGet("/devices/{id:int}", (int id, DeviceService service) => { + if (id <= 0) + { + return Results.BadRequest(new { message = "Device ID must be greater than zero." }); + } + var device = service.GetDevice(id); return device is null @@ -199,13 +204,13 @@ "application/dicom", StringComparison.OrdinalIgnoreCase) != true) { - await repository.AddAuditEventAsync("dicom.inspect", "unsupported-media-type", cancellationToken); + await TryAuditAsync(repository, "dicom.inspect", "unsupported-media-type", cancellationToken); return Results.StatusCode(StatusCodes.Status415UnsupportedMediaType); } if (request.ContentLength is > MaxDicomUploadBytes) { - await repository.AddAuditEventAsync("dicom.inspect", "payload-too-large", cancellationToken); + await TryAuditAsync(repository, "dicom.inspect", "payload-too-large", cancellationToken); return Results.StatusCode(StatusCodes.Status413PayloadTooLarge); } @@ -233,7 +238,7 @@ if (total == 0) { - await repository.AddAuditEventAsync("dicom.inspect", "empty-body", cancellationToken); + await TryAuditAsync(repository, "dicom.inspect", "empty-body", cancellationToken); return Results.BadRequest(new { message = "A DICOM request body is required." }); } @@ -242,15 +247,34 @@ try { var inspection = service.Inspect(buffer); - await repository.AddInspectionAsync(inspection, cancellationToken); - await repository.AddAuditEventAsync("dicom.inspect", "success", cancellationToken); + try + { + await repository.AddInspectionAsync(inspection, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + await TryAuditAsync(repository, "dicom.inspect", "success", cancellationToken); return Results.Ok(inspection); } catch (FellowOakDicom.DicomFileException) { - await repository.AddAuditEventAsync("dicom.inspect", "invalid-dicom", cancellationToken); + await TryAuditAsync(repository, "dicom.inspect", "invalid-dicom", cancellationToken); return Results.BadRequest(new { message = "The request body is not a readable DICOM file." }); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception) + { + await TryAuditAsync(repository, "dicom.inspect", "processing-error", CancellationToken.None); + return Results.Problem( + statusCode: StatusCodes.Status500InternalServerError, + title: "DICOM inspection failed.", + detail: "The DICOM document could not be processed."); + } }).RequireRateLimiting("api-write"); app.MapGet("/dicom/admin/inspections", async ( @@ -262,11 +286,11 @@ { if (!IsAuthorized(request, configuration)) { - await repository.AddAuditEventAsync("dicom.admin.inspections", "unauthorized", cancellationToken); + await TryAuditAsync(repository, "dicom.admin.inspections", "unauthorized", cancellationToken); return Results.Unauthorized(); } - await repository.AddAuditEventAsync("dicom.admin.inspections", "success", cancellationToken); + await TryAuditAsync(repository, "dicom.admin.inspections", "success", cancellationToken); var records = await repository.GetRecentAsync(take ?? 25, cancellationToken); return Results.Ok(records); }).RequireRateLimiting("api-write"); @@ -291,6 +315,22 @@ app.Run(); +static async Task TryAuditAsync(IDicomMetadataRepository repository, string eventType, string outcome, CancellationToken cancellationToken) +{ + try + { + await repository.AddAuditEventAsync(eventType, outcome, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch + { + // Audit persistence must never turn a request into an application failure. + } +} + static bool IsAuthorized(HttpRequest request, IConfiguration configuration) { var configuredKey = configuration["Security:ApiKey"];