WriteAsJsonAsync doesn't write to the response body when used inside a RequestTimeoutPolicy of the RequestTimeoutsMiddleware #58643
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\