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

Fix IAsyncEnumerable controller methods to allow setting headers #57924

Merged
merged 4 commits into from
Sep 20, 2024

Conversation

BrennanConroy
Copy link
Member

Fix IAsyncEnumerable controller methods to allow setting headers

Description

When writing Json responses in MVC, we started flushing headers immediately before using JsonSerializer in order to avoid some unnecessary buffering in Kestrel when writing starts before headers are flushed. This had the unintended side-effect of making IAsyncEnumerable<T> MVC methods no longer able to set headers due to the fact that the whole method becomes the IAsyncEnumerable and we pass that directly to JsonSerializer, which means we've already flushed headers by the time the MVC method starts running. Resulting in an exception on the server and a bad response on the client.

Fixes #57895

Customer Impact

MVC methods that returned IAsyncEnumerable<T> will now fail if they modify response headers.

Regression?

  • Yes
  • No

Regressed from 8.0

Risk

  • High
  • Medium
  • Low

Small targeted fix. Added test coverage for scenario.

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

@BrennanConroy BrennanConroy added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Sep 17, 2024
@BrennanConroy BrennanConroy requested a review from a team as a code owner September 17, 2024 18:36
@BrennanConroy BrennanConroy added the Servicing-approved Shiproom has approved the issue label Sep 17, 2024
@halter73
Copy link
Member

Should we do something for minimal APIs? I don't think it's as important as MVC because you cannot yield return from a lambda which are typically used in minimal APIs.

CS1621: The yield statement cannot be used inside an anonymous method or lambda expression.

However, you can run into the with a named function which isn't super rare. And at that point, the repro code looks very similar to what you have for an MVC action.

var app = WebApplication.Create(args);
app.MapGet("/", Test);
app.Run();

async IAsyncEnumerable<string> Test(HttpContext context)
{
    var testValue = await myService.GetHeaderValueAsync();
    context.Response.Headers["Test"] = testValue;

    await foreach (var item in myService.GetItemsAsync())
    {
        yield return item;
    }
}

I agree that we probably don't need to update WriteAsJsonAsync considering the code required to run into this issue would be far more complicated and unlikely than code that achieved the same thing without issue. You would need to write something like this:

app.MapGet("/", async (HttpContext context) =>
{
    async IAsyncEnumerable<string> Test()
    {
        var testValue = await myService.GetHeaderValueAsync();
        context.Response.Headers["Test"] = testValue;

        await foreach (var item in myService.GetItemsAsync())
        {
            yield return item;
        }
    }

    await context.Response.WriteAsJsonAsync(Test());
});

Instead of just:

app.MapGet("/", async (HttpContext context) =>
{
    async IAsyncEnumerable<string> Test()
    {
        await foreach (var item in myService.GetItemsAsync())
        {
            yield return item;
        }
    }

    var testValue = await myService.GetHeaderValueAsync();
    context.Response.Headers["Test"] = testValue;

    await context.Response.WriteAsJsonAsync(Test());
});

I suppose with weird enough layering it's possible that someone might try to mutate the response in an IAsyncEnumerable generator, but I think the risk goes significantly down when the generator isn't the MVC action or minimal route handler itself.

@BrennanConroy
Copy link
Member Author

Pretty sure minimal just calls into WriteAsJsonAsync, so looks like we'll want to fix the extension methods as well.

@wtgodbe wtgodbe merged commit 92376ce into release/9.0-rc2 Sep 20, 2024
25 checks passed
@wtgodbe wtgodbe deleted the brecon/startasync branch September 20, 2024 23:42
@dotnet-policy-service dotnet-policy-service bot added this to the 9.0-rc2 milestone Sep 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates Servicing-approved Shiproom has approved the issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants