Search code examples
c#asp.net-coreasp.net-core-8

How to return HttpProblemDetails from an ASP.NET Core 8 Minimal API when using [AsParameters]?


I'm trying to create an ASP.NET Core 8 Minimal API endpoint and I'm trying to get it to return HttpProblemDetails when the query string parameters are invalid. Of course, I can implement my own validation, but I'm trying to leverage the framework for this.

Basically, if I have a query string parameter (for example an integer) and the input is not a valid integer, then I do get back a 'bad request', but without any body. This makes it difficult for the consumer to understand what exactly is wrong.

Here's a repro as a C# unit test (I'm using TestServer, but I get the same result when I use kestrel):

using System.Net.Http;
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shouldly;

namespace TestProject1
{
    public class HttpProblemDetailsTests
    {
        [Fact]
        public async Task Should_return_problem_details_if_querystring_not_provided()
        {
            var (host, testClient) = await SetupWebApp();

            var response = await testClient.GetAsync("/problem");
            response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
            var text = await response.Content.ReadAsStringAsync();
            
            // Fails on this line
            text.ShouldNotBeEmpty();

            await host.StopAsync();
        }
        
        [Fact]
        public async Task Returns_ok_if_querystring_provided()
        {
            var (host, testClient) = await SetupWebApp();

            var response = await testClient.GetAsync("/problem?abc=123");

            response.StatusCode.ShouldBe(HttpStatusCode.OK);
            await host.StopAsync();
        }

        private async Task<(IHost host, HttpClient testClient)> SetupWebApp()
        {
            var b = new HostBuilder()
                .ConfigureWebHost(c =>
                {
                    c.UseTestServer();
                    c.ConfigureServices(services =>
                    {
                        services.AddProblemDetails(); 
                        services.AddRouting();
                    });
                    c.Configure(app =>
                    {
                        app.UseExceptionHandler();

                        app.UseRouting();
                        app.UseEndpoints(endpoints =>
                        {
                            endpoints.MapGet("/problem", Problem);
                        });
                    });
                });

            var host = await b.StartAsync();

            var testClient = host.GetTestClient();
            testClient.DefaultRequestHeaders.Add("Accept", "application/json");
            return (host, testClient);
        }

        public class MyParameters
        {
            public int Abc { get; set; }
        }

        private async Task<IResult> Problem([AsParameters] MyParameters p)
        {
            return Results.Text("Hello, World!");
        }
    }
}

I have tried several other things:

  1. Add my own IProblemDetailsWriter implementation, but it doesn't seem to hit this
  2. Use [FromQuery] and not [AsParameters]. This produces the same result
  3. I've followed the example here, but that also produces the exact same result: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/handle-errors?view=aspnetcore-8.0#problem-details

Solution

  • You'll need to throw on a bad request first:

    var builder = WebApplication.CreateBuilder(args);
    builder.Services.Configure<RouteHandlerOptions>(options =>
    {
        options.ThrowOnBadRequest = true;
    });
    

    Then, once an exception is thrown. You should be able to wire an exception handler using middleware.

    app.UseExceptionHandler("/error");
    
    app.Map("/error", (HttpContext context) =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
        if (exception is BadHttpRequestException badRequestException)
        {
            return Results.BadRequest(new ErrorResponse
            {
                Error = "Invalid Parameter",
                Details = badRequestException.Message
            });
        }
    
        return Results.Problem("An unexpected error occurred.");
    });
    
    

    The idea is to capture as much detail as you can and return a response with a body.