diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/ServiceOwnerOnBehalfOfPersonMiddleware.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/ServiceOwnerOnBehalfOfPersonMiddleware.cs index 7e991e06e..b09be9aab 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/ServiceOwnerOnBehalfOfPersonMiddleware.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/ServiceOwnerOnBehalfOfPersonMiddleware.cs @@ -37,7 +37,7 @@ public Task InvokeAsync(HttpContext context) if (!NorwegianPersonIdentifier.TryParse(endUserIdQuery.First(), out var endUserId)) { context.Response.StatusCode = StatusCodes.Status400BadRequest; - context.Response.WriteAsJsonAsync(context.ResponseBuilder( + context.Response.WriteAsJsonAsync(context.GetResponseOrDefault( context.Response.StatusCode, [ new("EndUserId", diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/UserTypeValidationMiddleware.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/UserTypeValidationMiddleware.cs index 96642cc5b..eb61f7f17 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/UserTypeValidationMiddleware.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/Authentication/UserTypeValidationMiddleware.cs @@ -21,7 +21,7 @@ public async Task InvokeAsync(HttpContext context) if (userType == UserIdType.Unknown) { context.Response.StatusCode = StatusCodes.Status403Forbidden; - await context.Response.WriteAsJsonAsync(context.ResponseBuilder( + await context.Response.WriteAsJsonAsync(context.GetResponseOrDefault( context.Response.StatusCode, [ new("Type", diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs index cb0b0741a..f4fb45977 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs @@ -5,6 +5,7 @@ internal static class Constants internal const string IfMatch = "If-Match"; internal const string Authorization = "Authorization"; internal const string CurrentTokenIssuer = "CurrentIssuer"; + internal const int MaxRequestBodySize = 100_000; internal static class SwaggerSummary { diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/Extensions/ErrorResponseBuilderExtensions.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/Extensions/ErrorResponseBuilderExtensions.cs index 08ca550e4..e7f1d9be2 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Common/Extensions/ErrorResponseBuilderExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/Extensions/ErrorResponseBuilderExtensions.cs @@ -6,16 +6,41 @@ namespace Digdir.Domain.Dialogporten.WebApi.Common.Extensions; internal static class ErrorResponseBuilderExtensions { - public static object ResponseBuilder(this HttpContext ctx, int statusCode, List? failures = null) => - ResponseBuilder(failures ?? [], ctx, statusCode); + public static ProblemDetails DefaultResponse(this HttpContext ctx, int? statusCode = null) => new() + { + Title = "An error occurred while processing the request.", + Detail = "Something went wrong during the request.", + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1", + Status = statusCode ?? ctx.Response.StatusCode, + Instance = ctx.Request.Path, + Extensions = { { "traceId", Activity.Current?.Id ?? ctx.TraceIdentifier } } + }; + + public static ProblemDetails GetResponseOrDefault(this HttpContext ctx, int statusCode, + List? failures = null) => + ctx.ResponseBuilder(failures, statusCode) ?? ctx.DefaultResponse(statusCode); public static object ResponseBuilder(List failures, HttpContext ctx, int statusCode) + => ctx.ResponseBuilder(failures, statusCode) ?? ctx.DefaultResponse(statusCode); + + public static ProblemDetails? ResponseBuilder(this HttpContext ctx, List? failures = null, int? statusCode = null) { - var errors = failures + var errors = failures? .GroupBy(f => f.PropertyName) - .ToDictionary(x => x.Key, x => x.Select(m => m.ErrorMessage).ToArray()); + .ToDictionary(x => x.Key, x => x.Select(m => m.ErrorMessage).ToArray()) + ?? []; + + statusCode ??= ctx.Response.StatusCode; return statusCode switch { + StatusCodes.Status413PayloadTooLarge => new ProblemDetails + { + Title = $"Payload too large. The maximum allowed size is {Constants.MaxRequestBodySize} bytes.", + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.11", + Status = statusCode, + Instance = ctx.Request.Path, + Extensions = { { "traceId", Activity.Current?.Id ?? ctx.TraceIdentifier } } + }, StatusCodes.Status400BadRequest => new ValidationProblemDetails(errors) { Title = "One or more validation errors occurred.", @@ -73,15 +98,7 @@ public static object ResponseBuilder(List failures, HttpConte Instance = ctx.Request.Path, Extensions = { { "traceId", Activity.Current?.Id ?? ctx.TraceIdentifier } } }, - _ => new ProblemDetails - { - Title = "An error occurred while processing the request.", - Detail = "Something went wrong during the request.", - Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1", - Status = ctx.Response.StatusCode, - Instance = ctx.Request.Path, - Extensions = { { "traceId", Activity.Current?.Id ?? ctx.TraceIdentifier } } - } + _ => null }; } } diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/Extensions/GlobalExceptionHandler.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/Extensions/GlobalExceptionHandler.cs index a082715d7..8876ef6fe 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Common/Extensions/GlobalExceptionHandler.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/Extensions/GlobalExceptionHandler.cs @@ -9,16 +9,26 @@ internal sealed class GlobalExceptionHandler : IExceptionHandler public async ValueTask TryHandleAsync(HttpContext ctx, Exception exception, CancellationToken cancellationToken) { - var http = $"{ctx.Request.Scheme}: {ctx.Request.Method} {ctx.Request.Path}"; - var type = exception.GetType().Name; - var error = exception.Message; - var logger = ctx.Resolve>(); - logger.LogError(exception, "{@Http}{@Type}{@Reason}", http, type, error); - ctx.Response.StatusCode = exception is IUpstreamServiceError - ? StatusCodes.Status502BadGateway - : StatusCodes.Status500InternalServerError; + ctx.Response.StatusCode = exception switch + { + BadHttpRequestException badHttpRequestException => badHttpRequestException.StatusCode, + IUpstreamServiceError => StatusCodes.Status502BadGateway, + _ => StatusCodes.Status500InternalServerError + }; + ctx.Response.ContentType = "application/problem+json"; - await ctx.Response.WriteAsJsonAsync(ctx.ResponseBuilder(ctx.Response.StatusCode), cancellationToken); + var response = ctx.ResponseBuilder(); + + if (ctx.Response.StatusCode >= 500 || response is null) + { + var http = $"{ctx.Request.Scheme}: {ctx.Request.Method} {ctx.Request.Path}"; + var type = exception.GetType().Name; + var error = exception.Message; + var logger = ctx.Resolve>(); + logger.LogError(exception, "{@Http}{@Type}{@Reason}", http, type, error); + } + + await ctx.Response.WriteAsJsonAsync(response ?? ctx.DefaultResponse(), cancellationToken); return true; } } diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs index 96906a286..b439574cb 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs @@ -70,10 +70,10 @@ public async Task Patch( if (!dialogQueryResult.TryPickT0(out var dialog, out var errors)) { return errors.Match( - notFound => NotFound(HttpContext.ResponseBuilder(StatusCodes.Status404NotFound, + notFound => NotFound(HttpContext.GetResponseOrDefault(StatusCodes.Status404NotFound, notFound.ToValidationResults())), validationFailed => - BadRequest(HttpContext.ResponseBuilder(StatusCodes.Status400BadRequest, + BadRequest(HttpContext.GetResponseOrDefault(StatusCodes.Status400BadRequest, validationFailed.Errors.ToList()))); } @@ -88,12 +88,12 @@ public async Task Patch( var result = await _sender.Send(command, ct); return result.Match( success => (IActionResult)NoContent(), - notFound => NotFound(HttpContext.ResponseBuilder(StatusCodes.Status404NotFound, notFound.ToValidationResults())), - badRequest => BadRequest(HttpContext.ResponseBuilder(StatusCodes.Status400BadRequest, badRequest.ToValidationResults())), - validationFailed => BadRequest(HttpContext.ResponseBuilder(StatusCodes.Status400BadRequest, validationFailed.Errors.ToList())), - forbidden => new ObjectResult(HttpContext.ResponseBuilder(StatusCodes.Status403Forbidden, forbidden.ToValidationResults())), - domainError => UnprocessableEntity(HttpContext.ResponseBuilder(StatusCodes.Status422UnprocessableEntity, domainError.ToValidationResults())), - concurrencyError => new ObjectResult(HttpContext.ResponseBuilder(StatusCodes.Status412PreconditionFailed)) { StatusCode = StatusCodes.Status412PreconditionFailed } + notFound => NotFound(HttpContext.GetResponseOrDefault(StatusCodes.Status404NotFound, notFound.ToValidationResults())), + badRequest => BadRequest(HttpContext.GetResponseOrDefault(StatusCodes.Status400BadRequest, badRequest.ToValidationResults())), + validationFailed => BadRequest(HttpContext.GetResponseOrDefault(StatusCodes.Status400BadRequest, validationFailed.Errors.ToList())), + forbidden => new ObjectResult(HttpContext.GetResponseOrDefault(StatusCodes.Status403Forbidden, forbidden.ToValidationResults())), + domainError => UnprocessableEntity(HttpContext.GetResponseOrDefault(StatusCodes.Status422UnprocessableEntity, domainError.ToValidationResults())), + concurrencyError => new ObjectResult(HttpContext.GetResponseOrDefault(StatusCodes.Status412PreconditionFailed)) { StatusCode = StatusCodes.Status412PreconditionFailed } ); } } diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Program.cs b/src/Digdir.Domain.Dialogporten.WebApi/Program.cs index 77ea56e3a..30e60ba03 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Program.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Program.cs @@ -52,6 +52,11 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura { var builder = WebApplication.CreateBuilder(args); + builder.WebHost.ConfigureKestrel(kestrelOptions => + { + kestrelOptions.Limits.MaxRequestBodySize = Constants.MaxRequestBodySize; + }); + builder.Host.UseSerilog((context, services, configuration) => configuration .MinimumLevel.Warning() .ReadFrom.Configuration(context.Configuration)