Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(webapi): Limit Content-Length / request body size #1416

Merged
merged 1 commit into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidationFailure>? 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 } }
oskogstad marked this conversation as resolved.
Show resolved Hide resolved
};

public static ProblemDetails GetResponseOrDefault(this HttpContext ctx, int statusCode,
List<ValidationFailure>? failures = null) =>
ctx.ResponseBuilder(failures, statusCode) ?? ctx.DefaultResponse(statusCode);

public static object ResponseBuilder(List<ValidationFailure> failures, HttpContext ctx, int statusCode)
=> ctx.ResponseBuilder(failures, statusCode) ?? ctx.DefaultResponse(statusCode);

oskogstad marked this conversation as resolved.
Show resolved Hide resolved
public static ProblemDetails? ResponseBuilder(this HttpContext ctx, List<ValidationFailure>? 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())
?? [];
oskogstad marked this conversation as resolved.
Show resolved Hide resolved

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 } }
oskogstad marked this conversation as resolved.
Show resolved Hide resolved
},
StatusCodes.Status400BadRequest => new ValidationProblemDetails(errors)
{
Title = "One or more validation errors occurred.",
Expand Down Expand Up @@ -73,15 +98,7 @@ public static object ResponseBuilder(List<ValidationFailure> 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
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,26 @@ internal sealed class GlobalExceptionHandler : IExceptionHandler
public async ValueTask<bool> 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<ILogger<GlobalExceptionHandler>>();
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<ILogger<GlobalExceptionHandler>>();
logger.LogError(exception, "{@Http}{@Type}{@Reason}", http, type, error);
}

await ctx.Response.WriteAsJsonAsync(response ?? ctx.DefaultResponse(), cancellationToken);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ public async Task<IActionResult> Patch(
if (!dialogQueryResult.TryPickT0(out var dialog, out var errors))
{
return errors.Match<IActionResult>(
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())));
}

Expand All @@ -88,12 +88,12 @@ public async Task<IActionResult> 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 }
);
}
}
5 changes: 5 additions & 0 deletions src/Digdir.Domain.Dialogporten.WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading