Home > Net >  ASP.NET Core async request handler always responds with 200 OK and empty body if it's a separat
ASP.NET Core async request handler always responds with 200 OK and empty body if it's a separat

Time:08-18

I'm using the ASP.NET Core 6 minimal APIs to build an API. After extracting anonymous/lambda handlers from MapGet into their own methods, some endpoints act erroneously by always returning a 200 (OK) response with an empty body.

Not sure whether I've missed some gotcha regarding how async works in C# or whether this is a bug with ASP.NET Core.

Reproducible example

Following is a minimal example where endpoints /a and /b execute similar methods, but one of them never responds as expected. To run the example, just copy the code into a new project created with dotnet new web.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

async Task<int> longRunningValueGenerator() {
    await Task.Delay(1000);
    return 4; // chosen by a fair dice roll
              // guaranteed to be random
}

async Task<IResult> getValueA(HttpContext http) {
    if(http.Request.Query["secret"] != "test") {
        return Results.Unauthorized();
    }

    var value = await longRunningValueGenerator();
    return Results.Ok(new {
        Value = value
    });
}

app.MapGet("/", () => "get /a or /b with secret=test");
app.MapGet("/a", getValueA);
app.MapGet("/b", async (HttpContext http) => {
    if(http.Request.Query["secret"] != "test") {
        return Results.Unauthorized();
    }

    var value = await longRunningValueGenerator();
    return Results.Ok(new {
        Value = value
    });
});
app.Run();

Some observations

  • This issue starts occurring once the secret check is added. This is just for example, adding different code also triggers this behavior.
  • It still takes a second (as determined by the delay) for the server to respond if secret is correct, even for getValueA.
  • /a will never respond with a different status code other than 200, even if secret is incorrect.
  • Non-async handlers that are separate methods work as expected.

CodePudding user response:

There's a couple of overloads for MapGet:

The difference between these two signatures is in that last parameter, Delegate vs RequestDelegate. Delegate represents any callable function, but RequestDelegate represents a function with one parameter, HttpContext, that returns a Task.

In your example, the getValueA function works for both of these delegates: it's any callable function and it's a function that takes HttpContext. For reasons beyond my understanding, the compiler picks the RequestDelegate version of MapGet, and effectively throws away the IResult value that it isn't expecting.

There might be a few options for fixing this, but the following solution should work well here. It changes your getValueA signature and takes advantage of parameter-binding for secret:

async Task<IResult> getValueA([FromQuery] string secret)
{
    if (secret != "test")
    {
        return Results.Unauthorized();
    }

    var value = await longRunningValueGenerator();
    return Results.Ok(new
    {
        Value = value
    });
}

This change means that the version of MapGet that takes a RequestDelegate is no longer a candidate, and therefore the Delegate version is called and processed correctly.

CodePudding user response:

This is a known issue with .NET 6 and minimal APIs and it has to do with overload resolution and what gets inferred by default https://github.com/dotnet/aspnetcore/issues/39956.

It has been fixed in .NET 7, we may backport to .NET 6 (you can request a back port on the issue).

  • Related