dotnet / dotnet/aspnetcore

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

Open
#58,643 1 comment 0 reactions 0 assignees View on GitHub
area-middleware
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 5h
Merged PRs (30d)
276

Description

### Is there an existing issue for this?

- [x] 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\
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.