Skip to content

Conversation

@halter73
Copy link
Member

@halter73 halter73 commented Apr 7, 2025

Most of the time, this should return an already-completed ValueTask, and not make much of a difference, but it can make a difference for someone using the ConnectCallback with a Stream type that implements DisposeAsync instead of Dispose(bool).

You can take a look at modelcontextprotocol/csharp-sdk#225 to see the conversation when I first noticed that SocketsHttpHandler wasn't calling DisposeAsync. If it had, it would have saved me some debugging.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

Comments suppressed due to low confidence (1)

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs:114

  • Using GetAwaiter().GetResult() here may lead to blocking if the DisposeAsync ValueTask is not already completed. If the underlying stream might perform asynchronous operations, consider ensuring it returns an already-completed ValueTask or document why blocking here is safe.
_stream.DisposeAsync().AsTask().GetAwaiter().GetResult();

@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

{
GC.SuppressFinalize(this);
_stream.Dispose();
_stream.DisposeAsync().AsTask().GetAwaiter().GetResult();
Copy link
Member

@stephentoub stephentoub Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this makes a difference, that suggests there's a Stream implementation being used that has additional functionality in its DisposeAsync that it doesn't have in its Dispose, sync vs async aside. Isn't the root of the issue here that the stream in question is overriding DisposeAsync but not Dispose?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the root of the issue here that the stream in question is overriding DisposeAsync but not Dispose?

It is, but shouldn't you be allowed to override DisposeAsync instead of Dispose?

Most of the time, this should return an already-completed ValueTask, and not make much of a difference, but it can make a difference for someone using the ConnectCallback with a Stream type that implements DisposeAsync instead of Dispose(bool).

You can take a look at modelcontextprotocol/csharp-sdk#225 to see the conversation when I first noticed that SocketsHttpHandler wasn't calling DisposeAsync. If it had, it would have saved me some debugging.

Copy link
Member

@stephentoub stephentoub Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, but shouldn't you be allowed to override DisposeAsync instead of Dispose?

It's very unintuitive if Dispose and DisposeAsync have different functionality, just as it's very unintuitive if Read/ReadAsync, Write/WriteAsync, etc., have different functionality that's separate from the "are operations performed synchronously or asynchronously" aspect.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed change also means that, if there was an operation that could be performed using synchronous I/O, it's now being forced into an async-over-sync path. Before, if it could avoid the sync-over-async in the Dispose implementation, it would.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I figured it'd be nicer to lazy Stream implementors like me to Have HttpConnection call DisposeAsync instead, since that also calls Dispose (bool) as long as you call base.DisposeAsync(), but I do see how that could be a perf trap given the sync-over-async. Not that synchronously blocking would be amazing either, but it might be one less thread per call if Dispose is implemented without doing sync-over-async itself.

It would be even better if HttpClient and SocketsHttpHandler implemented IAsyncDisposable, but that would be a far more drastic change.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants