1

I have an ASP.NET Core Web API controller action that returns a 201 Created response with a custom type, and I also have a custom exception handler that writes ProblemDetails for 400–500 errors.

The action looks like this:

[Authorize]
[HttpPost]
//[ProducesResponseType(typeof(TaskItemCreatedResponse), StatusCodes.Status201Created)]
//[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
//[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> CreateTaskItem(
    [FromBody] CreateTaskItemRequest query,
    CancellationToken cancellationToken = default)
{
    var taskItem = await TaskItem.Create(query, usersRepository, cancellationToken);
    await taskItemsRepository.AddTaskItem(taskItem, cancellationToken);
    await unitOfWorkRepository.SaveChangesAsync(cancellationToken);

    return CreatedAtAction(
        nameof(CreateTaskItem), 
        new { id = taskItem.Id }, 
        new TaskItemCreatedResponse(
            taskItem.Id,
            taskItem.Title,
            taskItem.Description,
            taskItem.IsCompleted,
            taskItem.UserId));
}

If I uncomment the line with [ProducesResponseType(typeof(TaskItemCreatedResponse), StatusCodes.Status201Created)] then my exception handler fails with:

Unable to find a registered IProblemDetailsWriter that can write to the given context

However if I use ProducesResponseType for 400 or 401 - or ProducesResponseType for 201 but without the typeof(TaskItemCreatedResponse) part it works fine.

As far as I know, the problem arises only if I use ProducesResponseType with 2xx status codes

How can I use [ProducesResponseType(typeof(TaskItemCreatedResponse), 201)] while keeping my custom ProblemDetails exception handler working for error responses?

Edit: My CustomExceptionHandler

public class CustomExceptionHandler(
    IProblemDetailsService problemDetailService,
    ILogger<CustomExceptionHandler> logger, 
    IExceptionMapper mapper) 
    : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext, 
        Exception exception,
        CancellationToken cancellationToken = default)
    {
        logger.LogError(
            exception, "Exception occurred: {Message}", exception.Message);

        var problemDetails = mapper.MapException(httpContext, exception);

        try
        {
            await problemDetailService.WriteAsync(
            new ProblemDetailsContext
            {
                HttpContext = httpContext,
                Exception = exception,
                ProblemDetails = problemDetails
            });
        }
        catch (Exception e)
        {
            logger.LogError(e.Message);
        }

        return true;
    }
}

I use my ExceptionMapper that inherits IExceptionMapper and is registered through DI. This mapper maps my custom exceptions

IExceptionMapper

public interface IExceptionMapper
{
    ProblemDetails MapException(HttpContext httpContext, Exception exception);
}

ExceptionMapper

public class ExceptionMapper : IExceptionMapper
{
    public ProblemDetails MapException(
        HttpContext httpContext,
        Exception exception)
    {
        int statusCode;
        string title;
        var errors = new Dictionary<string, string[]>();

        switch(exception)
        {
            case ValidationException e:
                statusCode = 400;
                title = "Validation failed";
                errors = e.Errors
                    .GroupBy(e => e.PropertyName)
                    .ToDictionary(g => g.Key,
                    g => g.Select(e => e.ErrorMessage).ToArray());
                break;
            case BadRequestException:
                statusCode = 400;
                title = "Error caused by request";
                break;
            case UnauthorizedException:
                statusCode = 401;
                title = "Unauthorized access";
                break;
            case NotFoundException:
                statusCode = 404;
                title = "Resource cannot be found";
                break;
            default:
                statusCode = 500;
                title = "Internal server problem";
                break;
        }

        httpContext.Response.StatusCode = statusCode;

        var problemDetails = new ProblemDetails
        {
            Type = exception.GetType().Name,
            Status = statusCode,
            Title = title
        };

        if (errors.Count() == 0)
            problemDetails.Detail = exception.Message;
        else
            problemDetails.Extensions.Add("errors", errors);

        return problemDetails;
    }
}

Then everything is registered in ExceptionHandlingMiddlewareRegistration

public static class ExceptionHandlingMiddlewareRegistration
{
    public static void RegisterExceptionHandling(this IServiceCollection serviceCollection)
    {
        serviceCollection.AddExceptionHandler<CustomExceptionHandler>();
        serviceCollection.AddSingleton<IExceptionMapper, ExceptionMapper>();
        serviceCollection.AddProblemDetails(options =>
        {
            options.CustomizeProblemDetails = context =>
            {
                context.ProblemDetails.Instance =
                    $"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";

                context.ProblemDetails.Extensions.TryAdd("requestId", context.HttpContext.TraceIdentifier);

                Activity? activity = context.HttpContext.Features.Get<IHttpActivityFeature>()?.Activity;
                context.ProblemDetails.Extensions.TryAdd("traceId", activity?.Id);

                context.ProblemDetails.Extensions.Add(
                    "stackTrace",
                    context.Exception.StackTrace
                        .Split(
                            ["\r\n", "\n"],
                            StringSplitOptions.TrimEntries));
            };
        });
    }
}
New contributor
Ucrainex is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
1
  • 1
    "and I also have a custom exception handler" - Please edit your post to include the details of your custom exception handler, including both the implementation and how it's registered. Commented Nov 26 at 5:59

1 Answer 1

1

I've found a solution to this
I've made my custom implementation of IProblemDetailsService

public class ProblemDetailsWriter : IProblemDetailsService
{
    public async ValueTask WriteAsync(ProblemDetailsContext context)
    {
        var response = context.HttpContext.Response;

        response.ContentType = "application/json";

        response.StatusCode = context.ProblemDetails?.Status ?? 500;

        context.ProblemDetails.Instance = $"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";

        context.ProblemDetails.Extensions.TryAdd("requestId", context.HttpContext.TraceIdentifier);

        Activity? activity = context.HttpContext.Features.Get<IHttpActivityFeature>()?.Activity;
        context.ProblemDetails.Extensions.TryAdd("traceId", activity?.Id);

        context.ProblemDetails.Extensions.Add(
            "stackTrace",
            context.Exception.StackTrace
                .Split(
                    ["\r\n", "\n"],
                    StringSplitOptions.TrimEntries));

        var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
        await JsonSerializer.SerializeAsync(response.Body, context.ProblemDetails ?? new ProblemDetails
        {
            Status = 500,
            Title = "Unknown error"
        }, options);
    }
}

Then registered it into DI in my ExceptionHandlingMiddlewareRegistration like this
serviceCollection.AddSingleton<IProblemDetailsService, ProblemDetailsWriter>();

New contributor
Ucrainex is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.