question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

ASP.NET core GRPC server can terminate connections when `MAX_CONCURRENT_STREAMS` is breached

See original GitHub issue

Originally created at https://github.com/grpc/grpc-dotnet/issues/673 by @djluck


What version of gRPC and what language are you using?

grpc=v2.24.0 lang=C#

What operating system (Linux, Windows,…) and version?

Windows, v10.0.18362 Build 18362

What runtime / compiler are you using (e.g. .NET Core SDK version dotnet --info)

v3.0.100

What did you do?

When issuing many requests on a fresh/ idle connection, it’s possible to exceed the MAX_CONCURRENT_STREAMS setting of a server, causing the connection to fault and failing successfully processed streams. Exceeding the MAX_CONCURRENT_STREAMS on a fresh/ idle connection should be supported as the initial value is assumed to be unlimited until a peer has communicated it’s prefered limit.

This psuedo-code highlights how to re-create the issue on the client-side:

// This test fails about 1/3 times (yuk). Bit hacky but let's repeat the test a large number of times
// to verify everything is working correctly.
for (int i = 0; i < 5; i++)
{
	// Must use a fresh client for each iteration of the test. The GRPC server will indicate it's max concurrency
	// limit after the first request successfully completes. However, we are interested in making sure large bursts
	// on a connection that has not discovered this limit works as expected.
	using var c = GrpcChannel.ForAddress(new Uri("https://localhost:5001"));
	var client = new Health.HealthClient(c);
	
	var tasks = Enumerable.Range(1, 5)
		.Select(x => Task.Run(async () => await client.CheckAsync(new HealthCheckRequest())))
		.ToArray();

	await Task.WhenAll(tasks);
}

MAX_CONCURRENT_STREAMS should be set to a low value in the ASP.NET grpc server (in appsettings.json):

{
  "Kestrel": {
    "Limits": {
      "Http2" : {
        "MaxStreamsPerConnection" : 2
      }
    }
  }
}

What did you expect to see?

The streams that exceed the MAX_CONCURRENT_STREAMS limit should be closed and any DATA frames received by the server for these streams should result in a STREAM_CLOSED error as per the HTTP/2 RFC:

What did you see instead?

Streams that exceed the MAX_CONCURRENT_STREAMS had DATA frames in-flight which resulted in the entire TCP connection being torn down:

[10:13:55 VRB] Connection id "0HLRKB2HNVILT" received SETTINGS frame for stream ID 0 with length 12 and flags NONE
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" sending SETTINGS frame for stream ID 0 with length 0 and flags ACK
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" received WINDOW_UPDATE frame for stream ID 0 with length 4 and flags 0x0
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" received HEADERS frame for stream ID 1 with length 139 and flags END_HEADERS
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" received HEADERS frame for stream ID 3 with length 139 and flags END_HEADERS
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" received HEADERS frame for stream ID 5 with length 139 and flags END_HEADERS
[10:13:55 DBG] Connection id "0HLRKB2HNVILT": HTTP/2 stream error.
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2StreamErrorException: HTTP/2 stream ID 5 error (REFUSED_STREAM): A new stream was refused because this connection has reached its stream limit.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.StartStream()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.DecodeHeadersAsync(Boolean endHeaders, ReadOnlySequence`1& payload)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessHeadersFrameAsync[TContext](IHttpApplication`1 application, ReadOnlySequence`1& payload)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessFrameAsync[TContext](IHttpApplication`1 application, ReadOnlySequence`1& payload)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessRequestsAsync[TContext](IHttpApplication`1 application)
[10:13:55 INF] Request starting HTTP/2 POST https://localhost:5001/grpc.health.v1.Health/Check application/grpc
[10:13:55 INF] Request starting HTTP/2 POST https://localhost:5001/grpc.health.v1.Health/Check application/grpc
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" sending RST_STREAM frame for stream ID 5 with length 4 and flags 0x0
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" received HEADERS frame for stream ID 7 with length 139 and flags END_HEADERS
[10:13:55 DBG] Connection id "0HLRKB2HNVILT": HTTP/2 stream error.
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2StreamErrorException: HTTP/2 stream ID 7 error (REFUSED_STREAM): A new stream was refused because this connection has reached its stream limit.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.StartStream()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.DecodeHeadersAsync(Boolean endHeaders, ReadOnlySequence`1& payload)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessHeadersFrameAsync[TContext](IHttpApplication`1 application, ReadOnlySequence`1& payload)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessFrameAsync[TContext](IHttpApplication`1 application, ReadOnlySequence`1& payload)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessRequestsAsync[TContext](IHttpApplication`1 application)
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" sending RST_STREAM frame for stream ID 7 with length 4 and flags 0x0
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" received HEADERS frame for stream ID 9 with length 139 and flags END_HEADERS
[10:13:55 DBG] Connection id "0HLRKB2HNVILT": HTTP/2 stream error.
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2StreamErrorException: HTTP/2 stream ID 9 error (REFUSED_STREAM): A new stream was refused because this connection has reached its stream limit.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.StartStream()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.DecodeHeadersAsync(Boolean endHeaders, ReadOnlySequence`1& payload)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessHeadersFrameAsync[TContext](IHttpApplication`1 application, ReadOnlySequence`1& payload)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessFrameAsync[TContext](IHttpApplication`1 application, ReadOnlySequence`1& payload)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessRequestsAsync[TContext](IHttpApplication`1 application)
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" sending RST_STREAM frame for stream ID 9 with length 4 and flags 0x0
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" received SETTINGS frame for stream ID 0 with length 0 and flags ACK
[10:13:55 VRB] Connection id "0HLRKB2HNVILT" received DATA frame for stream ID 9 with length 5 and flags NONE
[10:13:55 DBG] Connection id "0HLRKB2HNVILT": HTTP/2 connection error.
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2ConnectionErrorException: HTTP/2 connection error (STREAM_CLOSED): The client sent a DATA frame to closed stream ID 9.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessDataFrameAsync(ReadOnlySequence`1& payload)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessFrameAsync[TContext](IHttpApplication`1 application, ReadOnlySequence`1& payload)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection.ProcessRequestsAsync[TContext](IHttpApplication`1 application)
[10:13:55 DBG] Connection id "0HLRKB2HNVILT" is closed. The last processed stream ID was 9.

The impact of faulting the entire connection is that some requests may have successfully processed and their CancellationToken may not trigger- this means it’s impossible to recover from this kind of error gracefully.

Anything else we should know about your project / environment?

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:14 (10 by maintainers)

github_iconTop GitHub Comments

1reaction
Tratchercommented, Nov 29, 2019

Ah, idle connections aren’t really special then, they’re just closed so a new one gets automatically created.

I’m not aware of any provision in the spec for a client to cache settings across connections. It makes sense that both clients start with a clean slate on each connection.

0reactions
Tratchercommented, Dec 17, 2019

Also keep in mind that 100 is the default stream limit for several other servers so this is not going to be an isolated issue.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Performance best practices with gRPC
Connection packet loss causes all calls to be blocked at the TCP layer. ServerGarbageCollection in client apps. The .NET garbage collector has ...
Read more >
Security considerations in gRPC for ASP.NET Core
The benefits of using TLS termination should be considered against the security risks of sending unsecured HTTP requests between apps in the ...
Read more >
When making Grpc request over http in .net core creates ...
ControllerActionInvoker.InvokeInnerFilterAsync() --- End of stack trace from previous location where exception was thrown --- at Microsoft.
Read more >
Keepalive
Options Availability Client Default Server Default KEEPALIVE_TIME Client and Server INT_MAX (Disabled) 27200000 (2 hours) KEEPALIVE_TIMEOUT Client and Server 20000 (20 seconds) 20000 (20 seconds) KEEPALIVE_WITHOUT_CALLS...
Read more >
Unable to make a connection between trivial C# gRPC ...
I have resolved this issue by using the native .Net gRPC library (instead of the ASP.Net Core based one). Share.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found