AspNetCore Mvc Controller keeps serializing IAsyncEnumerable after connection is closed
See original GitHub issueDescribe the bug
In https://github.com/dotnet/aspnetcore/issues/32483 (see also) a breaking change regarding the processing of IAsyncEnumerable
was announced. I wanted to test the new behavior using preview version 6.0.100-preview.6.21355.2.
Observed behavior
At first glance, it works as expected and data is returned to the client even before the processing is terminated. However, adding Console.WriteLine
reveals that the ASP keeps iterating over IAsyncEnumerable
for serialization even a long time after the client has closed the connection. This puts unnecessary load on the server because the result of that processing will never be fetched.
Expected behavior
After the client closed the connection the server should stop processing and hence stop iterating over IAsyncEnumerable
To Reproduce
Server
The server exposes an endpoint “Loop” which returns a list of 100000 ints. Each time a single int is fetched it will be logged in the console with a counter and the current timestamp.
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System;
namespace dot_net_api_streaming.Controllers
{
[ApiController]
[Route("[controller]")]
public class LoopController : ControllerBase
{
[HttpGet]
public IAsyncEnumerable<int> Get()
{
return GetInfiniteInts();
}
private async IAsyncEnumerable<int> GetInfiniteInts()
{
int index = 0;
while (index <100000)
{
Console.WriteLine($"{DateTime.Now}: {index}");
yield return index++;
}
}
}
}
Output
Please note that the server is processing for about a minute when this endpoint is called.
8/13/2021 11:51:18 AM: 1
8/13/2021 11:51:18 AM: 2
[...]
8/13/2021 11:52:19 AM: 99998
8/13/2021 11:52:19 AM: 99999
Client
The Python client calls the server’s exposed “Loop” endpoint, but only fetches the first 100 bytes and closes the connection afterwards. The client code terminates after about 100-200ms.
import urllib.request
import time
import ssl
request_url = 'https://localhost:5001/Loop'
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
print(f"Start request {request_url}")
start = time.time()
f = urllib.request.urlopen(request_url, context=ctx)
print(f.read(100).decode('utf-8'))
end = time.time()
print("Completed in {}s".format(round(end - start, 3)))
Output
Please not the the client closes the connection after less than a second while the server before was processing for about a minute.
Start request https://localhost:5001/Loop
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,3
Completed in 0.107s
Exceptions (if any)
There are no exceptions. The logs reveal the unexpected bahvior.
Further technical details
.NET SDK (reflecting any global.json): Version: 6.0.100-preview.6.21355.2 Commit: 7f8e0d76c0
Runtime Environment: OS Name: ubuntu OS Version: 20.04 OS Platform: Linux RID: ubuntu.20.04-x64 Base Path: /home/USER/.dotnet/sdk/6.0.100-preview.6.21355.2/
Host (useful for support): Version: 6.0.0-preview.6.21352.12 Commit: 770d630b28
.NET SDKs installed: 5.0.205 [/home/USER/.dotnet/sdk] 6.0.100-preview.6.21355.2 [/home/USER/.dotnet/sdk]
.NET runtimes installed: Microsoft.AspNetCore.App 5.0.8 [/home/USER/.dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.0-preview.6.21355.2 [/home/USER/.dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 5.0.8 [/home/USER/.dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.0-preview.6.21352.12 [/home/USER/.dotnet/shared/Microsoft.NETCore.App]
Issue Analytics
- State:
- Created 2 years ago
- Comments:12 (7 by maintainers)
Related to https://github.com/dotnet/runtime/issues/51176#issuecomment-818866190. Compiler-generated async enumerators will throw NotSupportedException if an attempt is made to dispose them while a
MoveNextAsync()
task is pending completion. It would seem like STJ is doing this when serialization is cancelled due to a cancellation token firing. It’s a bug and we need to fix it.Here’s the pattern we use for sending files https://github.com/dotnet/aspnetcore/blob/ff51fd7105a9003841215f1f3b0b8fc9e2998a67/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs#L142-L153. Same technique applies here.