Skip to content

WriteAsJsonAsync doesn't write to the response body when used inside a RequestTimeoutPolicy of the RequestTimeoutsMiddleware #58643

Open
@Tri125

Description

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

WriteAsJsonAsync will not write to the response body when used inside a RequestTimeoutPolicy of the RequestTimeoutsMiddleware.

For example, a user could be configuring the WriteTimeoutResponse delegate to return an error type for the policy.
The middleware will successfully execute the policy, the response status code will be correctly set but the response body will stay empty.

I investigated and I found that the problem is in the slow path of the extension method.
If a user doesn’t pass a cancellable CancellationToken to WriteAsJsonAsync, then it will use the token from the httpContext response.HttpContext.RequestAborted. If that token is cancelled then the call that it does to JsonSerializer.SerializeAsync will be cancelled.

In this scenario with the TimeoutMiddleware, when the delegate for the RequestTimeoutPolicy is executed the request is already aborted and the call to WriteAsJsonAsync will be immediately cancelled, which is why the response body remains empty.

As a workaround a user can create a new CancellationTokenSource in the scope of the delegate and pass its token to WriteAsJsonAsync.

context.Response.WriteAsync doesn't have this problem.

Expected Behavior

WriteAsJsonAsync should write to the request body when used inside the WriteTimeoutResponse delegate of a RequestTimeoutPolicy when called without a CancellationToken.

Steps To Reproduce

Here's a minimal repro case made with ASP.NET Core Web API.

Make sure to start the program without debugging or the TimeoutMiddleware will not be enabled.

// Program.cs

using Microsoft.AspNetCore.Http.Timeouts;
using System;

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRequestTimeouts(options =>
{
    options.DefaultPolicy = new RequestTimeoutPolicy
    {
        Timeout = TimeSpan.FromSeconds(2),
        WriteTimeoutResponse = async (HttpContext context) =>
        {
            var weather = new WeatherForecast
            (
                DateOnly.FromDateTime(DateTime.Now),
                Random.Shared.Next(-20, 55),
                summaries[Random.Shared.Next(summaries.Length)]
            );

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

var app = builder.Build();
app.UseRequestTimeouts();

// Configure the HTTP request pipeline.

app.MapGet("/", async (HttpContext context) =>
{
    await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
    return Results.Content("No timeout!", "text/plain");
});



app.Run();

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

Exceptions (if any)

No response

.NET Version

8.0.403

Anything else?

ASP.NET Core Web API
Visual Studio Version 17.11.5

.NET SDK:
 Version:           8.0.403
 Commit:            c64aa40a71
 Workload version:  8.0.400-manifests.e99c892e
 MSBuild version:   17.11.9+a69bbaaf5

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.22631
 OS Platform: Windows
 RID:         win-x64
 Base Path:   c:\program files\dotnet\sdk\8.0.403\

Metadata

Assignees

No one assigned

    Labels

    area-middlewareIncludes: URL rewrite, redirect, response cache/compression, session, and other general middlesware

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions