aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

RequestTimeoutsMiddleware doesn't seem to work outside of me manually throwing an OperationCancelledException

Open zack4485 opened this issue 1 year ago • 1 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Describe the bug

If I create a default ASP.NET Core 8 Web API project in Visual Studio and add the Request Timeouts Middleware with a default policy of 10 seconds I don't get an HTTP504 even if I put a Thread.Sleep(30000) in the API endpoint to induce a timeout.

Looking at RequestTimeoutsMiddleware.cs it seems that the only scenario in which I'd get back an empty response and the defined HTTP error code is if my code manually throws an OperationCancelledException.

I started a long-running API and then attached a debugger (to get past the if (Debugger.IsAttached) { return _next(context); } at line #31 in RequestTimeoutsMiddleware.cs) and if I step backward from a breakpoint at the finally{} block I land at await _next(context); (line #107)...

Importantly my WriteTimeoutResponse delegate specified in the timeout policy is never hit.

In other words it seems like the middleware may possibly trigger a timeout like it should (though in my testing it doesn't seem to do that either) but it definitely doesn't set the HTTP result code when an induced timeout occurs. If I had to guess I'm not getting the expected HTTP result code specifically because it's not actually inducing the timeout in the first place...using the provided sample code the configured 10-second timeout isn't honored.

It seems to me like what would be required is to call context.RequestAborted.Register() within the try block at line #101 and either move or duplicate the content of the catch()when() block to the delegate registered on the linkedCts.Token cancellaton token???

Expected Behavior

If my operation exceeds the configured timeout then the configured HTTP status code should be set.

Steps To Reproduce

  1. Create an ASP.NET Core Web API project in Visual Studio using the below code for the program.cs file (no other changes needed to the out-of-box project) using .NET 8.0
  2. Start the project without a debugger (else the middleware is bypassed)
  3. Invoke the test API (ex: using PowerShell Invoke-WebRequest -Uri 'https://localhost:44339/{weatherforecast}?timeout=30000')
using Microsoft.AspNetCore.Http.Timeouts;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddRequestTimeouts(options =>
{
    options.DefaultPolicy = new RequestTimeoutPolicy
    {
        Timeout = TimeSpan.FromSeconds(10),
        TimeoutStatusCode = StatusCodes.Status504GatewayTimeout,
        WriteTimeoutResponse = async (HttpContext context) => {
            Thread.Sleep(120000);
            System.Diagnostics.Debug.WriteLine("Entered the writetimeoutresponse delegate");
        }
    };
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseRequestTimeouts();

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

app.MapGet("/weatherforecast", (int timeout) =>
{
    // Generate something interesting for a response body JSON
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();

    // Recreate a "sleeping thread" behavior to trigger the demo
    var forevertask = new Task(async () =>
    {
        Thread.Sleep(timeout);
    });
    forevertask.Start();
    forevertask.Wait();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

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

Anything else?

Microsoft Visual Studio Enterprise 2022
Version 17.8.7
VisualStudio.17.Release/17.8.7+34601.278
Microsoft .NET Framework
Version 4.8.09032

Installed Version: Enterprise

ADL Tools Service Provider   1.0
This package contains services used by Data Lake tools

ASA Service Provider   1.0

ASP.NET and Web Tools   17.8.358.6298
ASP.NET and Web Tools

Azure App Service Tools v3.0.0   17.8.358.6298
Azure App Service Tools v3.0.0

Azure Data Lake Tools for Visual Studio   2.6.5000.0
Microsoft Azure Data Lake Tools for Visual Studio

Azure Functions and Web Jobs Tools   17.8.358.6298
Azure Functions and Web Jobs Tools

Azure Stream Analytics Tools for Visual Studio   2.6.5000.0
Microsoft Azure Stream Analytics Tools for Visual Studio

C# Tools   4.8.0-7.23572.1+7b75981cf3bd520b86ec4ed00ec156c8bc48e4eb
C# components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Common Azure Tools   1.10
Provides common services for use by Azure Mobile Services and Microsoft Azure Tools.

Microsoft Azure Hive Query Language Service   2.6.5000.0
Language service for Hive query

Microsoft Azure Stream Analytics Language Service   2.6.5000.0
Language service for Azure Stream Analytics

Microsoft Azure Tools for Visual Studio   2.9
Support for Azure Cloud Services projects

Microsoft JVM Debugger   1.0
Provides support for connecting the Visual Studio debugger to JDWP compatible Java Virtual Machines

NuGet Package Manager   6.8.1
NuGet Package Manager in Visual Studio. For more information about NuGet, visit https://docs.nuget.org/

Razor (ASP.NET Core)   17.8.3.2405201+d135dd8d2ec1c2fbdee220e8656b308694e17a4b
Provides languages services for ASP.NET Core Razor.

SQL Server Data Tools   17.8.120.1
Microsoft SQL Server Data Tools

ToolWindowHostedEditor   1.0
Hosting json editor into a tool window

TypeScript Tools   17.0.20920.2001
TypeScript Tools for Microsoft Visual Studio

Visual Basic Tools   4.8.0-7.23572.1+7b75981cf3bd520b86ec4ed00ec156c8bc48e4eb
Visual Basic components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Visual F# Tools   17.8.0-beta.23475.2+10f956e631a1efc0f7f5e49c626c494cd32b1f50
Microsoft Visual F# Tools

Visual Studio IntelliCode   2.2
AI-assisted development for Visual Studio.
.NET SDK:
 Version:           8.0.102
 Commit:            64f1bc458e
 Workload version:  8.0.100-manifests.8a11730e

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.22621
 OS Platform: Windows
 RID:         win-x64
 Base Path:   C:\Program Files\dotnet\sdk\8.0.102\

.NET workloads installed:
 Workload version: 8.0.100-manifests.8a11730e
There are no installed workloads to display.

Host:
  Version:      8.0.2
  Architecture: x64
  Commit:       1381d5ebd2

.NET SDKs installed:
  6.0.419 [C:\Program Files\dotnet\sdk]
  8.0.102 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

zack4485 avatar Feb 22 '24 21:02 zack4485

The middleware is designed for cooperative timeouts, not forceful ones. E.g. when the timeout fires it's not going to call Thread.Abort or anything, that ends up being too destabilizing. What it does is signal HttpContext.RequestAborted and it's up to observers to stop their work.

Replace forevertask with something like this:

await Task.Delay(timeout, httpContext.RequestAborted);

By flowing the RequestAborted token to the long running operations they know when the timeout happens and can stop themselves.

Tratcher avatar Feb 23 '24 17:02 Tratcher

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.