Repository: dotnet/runtime
Pull request #114226: [S.N.Quic] Observe exceptions in ResettableValueTaskSource
Author: login: antonfirsov
Labels: name: area-System.Net.Quic
Created: 2025-04-03T17:47:17Z | Merged: 2025-04-15T14:11:07Z

There are use cases of `QuicStream.WritesClosed` / `QuicStream.ReadsClosed` in .NET itself which only query the `IsCompletedSuccessfully` property:
- In SocketsHttpHandler: [here](https://github.com/dotnet/runtime/blob/43704746091afad3a3312ddb0f285d539db4b49b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs#L91) and [here](https://github.com/dotnet/runtime/blob/43704746091afad3a3312ddb0f285d539db4b49b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs#L117)
- In Kestre [here](https://github.com/dotnet/aspnetcore/blob/eb05c23e1a2c63890abf4e0947ad32672a74a3f2/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs#L226)

The current implementation would require those call sites to await the task or do the `_ = task.Exception` trick to avoid leaking unobserved exceptions. IMO this behavior is hard to communicate and the fact that we "misused" our own API is telling. It seems more reasonable if the implementation ensured that exceptions from those tasks are never forwarded to `TaskScheduler.UnobservedTaskException`.

This PR implements this behavior in `ResettableValueTaskSource`. Beyond that, it includes a refactor of the `TrySetException()` method removing its `final` parameter since it was never called with `final: false`, which felt confusing when working with the code.

Fixes #114128.

 Running #114225  against this PR locally reduced the number of unobserved exceptions to zero for me.


Linked issue #114128: TaskScheduler.UnobservedTaskException catches quic exceptions with kestrel combined with grpc + http3
### Description

I've got a grpc service that has been running for sometime using just http2. I recently enabled http3 to see if there would be any performance benefit or improvement in connectivity some users were facing. I noticed almost immediately some exceptions were being caught where previously there weren't any.

Three different exceptions, all slightly different seem to be thrown

```
2025-04-01T15:54:54.8403559-07:00	CRIT	[LogicBase]	[0]	System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. (Connection aborted by peer (256).)
 ---> System.Net.Quic.QuicException: Connection aborted by peer (256).
   at System.Net.Quic.QuicConnection.HandleEventShutdownInitiatedByPeer(_SHUTDOWN_INITIATED_BY_PEER_e__Struct& data)
   at System.Net.Quic.QuicConnection.HandleConnectionEvent(QUIC_CONNECTION_EVENT& connectionEvent)
   at System.Net.Quic.QuicConnection.NativeCallback(QUIC_HANDLE* connection, Void* context, QUIC_CONNECTION_EVENT* connectionEvent)
--- End of stack trace from previous location ---
   at System.Net.Quic.QuicConnection.AcceptInboundStreamAsync(CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal.QuicConnectionContext.AcceptAsync(CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   
  
   2025-04-01T15:54:54.8406298-07:00	CRIT	[LogicBase]	[0] System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. (Connection aborted by peer (256).)
 ---> System.Net.Quic.QuicException: Connection aborted by peer (256).
   at System.Net.Quic.ResettableValueTaskSource.TryComplete(Exception exception, Boolean final)
   at System.Net.Quic.QuicStream.HandleEventShutdownComplete(_SHUTDOWN_COMPLETE_e__Struct& data)
   at System.Net.Quic.QuicStream.HandleStreamEvent(QUIC_STREAM_EVENT& streamEvent)
   at System.Net.Quic.QuicStream.NativeCallback(QUIC_HANDLE* connection, Void* context, QUIC_STREAM_EVENT* streamEvent)
--- End of stack trace from previous location ---
   at System.Net.Quic.ResettableValueTaskSource.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token)
   at System.Net.Quic.QuicStream.ReadAsync(Memory`1 buffer, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal.QuicStreamContext.DoReceiveAsync()
   --- End of inner exception stack trace ---
   
   
   2025-04-01T15:54:54.8410699-07:00	CRIT	[LogicBase]	[0]	System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. (Connection aborted by peer (256).)
 ---> Microsoft.AspNetCore.Connections.ConnectionResetException: Connection aborted by peer (256).
 ---> System.Net.Quic.QuicException: Connection aborted by peer (256).
   at System.Net.Quic.ResettableValueTaskSource.TryComplete(Exception exception, Boolean final)
   at System.Net.Quic.QuicStream.HandleEventShutdownComplete(_SHUTDOWN_COMPLETE_e__Struct& data)
   at System.Net.Quic.QuicStream.HandleStreamEvent(QUIC_STREAM_EVENT& streamEvent)
   at System.Net.Quic.QuicStream.NativeCallback(QUIC_HANDLE* connection, Void* context, QUIC_STREAM_EVENT* streamEvent)
--- End of stack trace from previous location ---
   at System.Net.Quic.ResettableValueTaskSource.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token)
   at System.Net.Quic.QuicStream.ReadAsync(Memory`1 buffer, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal.QuicStreamContext.DoReceiveAsync()
   --- End of inner exception stack trace ---
   at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)
   at System.IO.Pipelines.Pipe.GetReadAsyncResult()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3ControlStream.HandleControlStream()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3ControlStream.ProcessRequestAsync[TContext](IHttpApplication`1 application)
   --- End of inner exception stack trace ---
   
   

```

There maybe more exceptions, but when it happens so many are generated its hard for me to tell.



### Reproduction Steps

I'm not exactly sure what's happening on the clientside to produce the issue, but the serverside setup is almost exactly a basic copy of 

https://protobuf-net.github.io/protobuf-net.Grpc/gettingstarted

with http3 enabled.

### Expected behavior

Exceptions are handled in the layer they are being generated

### Actual behavior

Exceptions are going uncaught

### Regression?

_No response_

### Known Workarounds

_No response_

### Configuration

_No response_

### Other information

I'm currently using .net8, but I also tried upgrading to .net9 but the issue persisted.
Possibly related to https://github.com/dotnet/runtime/issues/80111 ?
Linked issue comments: user: login: dotnet-policy-service[bot]
created_at: 2025-04-01T23:08:01Z
body: Tagging subscribers to this area: @dotnet/ncl
See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
<!-- Policy app identification https://img.shields.io/static/v1?label=PullRequestIssueManagement. -->
author_association: CONTRIBUTOR
user: login: ManickaP
created_at: 2025-04-02T07:48:56Z
body: This likely overlaps with the experiments in https://github.com/dotnet/runtime/pull/105336.

In 9.0, I fixed the issues for the client side in https://github.com/dotnet/runtime/pull/104894. This might need work in Kestrel and QUIC transport code, maybe even gRPC. But the first step will be cleaning unobserved exceptions from Kestrel.

Since this is not affecting running code, it has a lower priority (i.e. its uncomfortable, but doesn't crash your service).
author_association: MEMBER
user: login: BrennanConroy
created_at: 2025-04-02T17:59:05Z
body: The 3 stacks listed above all have Kestrel as the calling code that isn't catching the exception. I think we can move this issue to dotnet/aspnetcore.
author_association: MEMBER
user: login: ManickaP
created_at: 2025-04-02T18:23:35Z
body: Sure, I'll be glad to hand it over 😄
Just a few hints since the S.N.Quic Task are mostly backed by `ManualResetValueTaskSourceCore` and handled manually:
- the stream `ReadAsync` call stack is likely https://github.com/dotnet/aspnetcore/blob/eb05c23e1a2c63890abf4e0947ad32672a74a3f2/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs#L226 ==> `_stream.ReadsClosed` needs to be observed
- the connection `AcceptInboundStreamAsync` got follow up fix in https://github.com/dotnet/runtime/pull/112190, which is only in [10.0 preview.2](https://github.com/dotnet/runtime/commit/3998832b9727592c3a9e863d962e97f6ba61d42c) @xeropresence would you be able to test it out with this version?


author_association: MEMBER
user: login: antonfirsov
created_at: 2025-04-02T19:38:27Z
body: Is it expected that whenever a user touches `WritesClosed` and `ReadsClosed` they should also observe the exceptions?
Note that this is a somewhat arbitrary behavior since the Tasks exposed by those properties are lazily initialized. (If you never touch them you can forget they exist from the POV of unobserved exceptions.) Even SocketsHttpHandler HTTP/3 code suffers from the problem of not observing `WritesClosed` exceptions (this is why we see them in  https://github.com/dotnet/runtime/pull/105336).

Shouldn't we rather implement a counterpart of #112190 for those tasks? Gonna be tricky because of the lazy init, but I would be happy to give it a try. @ManickaP thoughts?
author_association: CONTRIBUTOR
user: login: ManickaP
created_at: 2025-04-03T08:04:43Z
body: > Is it expected that whenever a user touches WritesClosed and ReadsClosed they should also observe the exceptions?|

Yes.

> Shouldn't we rather implement a counterpart of https://github.com/dotnet/runtime/pull/112190 for those tasks?

That would be actually really nice, if it's possible and has a reasonable solution. But I wouldn't sweat it if it proves to be too complicated. It's not super critical.


author_association: MEMBER
user: login: xeropresence
created_at: 2025-04-04T02:01:37Z
body: > Sure, I'll be glad to hand it over 😄 Just a few hints since the S.N.Quic Task are mostly backed by `ManualResetValueTaskSourceCore` and handled manually:
> 
> * the stream `ReadAsync` call stack is likely https://github.com/dotnet/aspnetcore/blob/eb05c23e1a2c63890abf4e0947ad32672a74a3f2/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs#L226 ==> `_stream.ReadsClosed` needs to be observed
> * the connection `AcceptInboundStreamAsync` got follow up fix in [[QUIC] Observe exceptions from _connectionCloseTcs #112190](https://github.com/dotnet/runtime/pull/112190), which is only in [10.0 preview.2](https://github.com/dotnet/runtime/commit/3998832b9727592c3a9e863d962e97f6ba61d42c) [@xeropresence](https://github.com/xeropresence) would you be able to test it out with this version?

I can try to get around to it this weekend.

I noticed another exception, but this one wasn't being caught in the task.unobservedtaskexception, is this related to the exceptions present in this existing issue, should I create a new issue, or is this intended behavior.

```
crit: Microsoft.AspNetCore.Server.Kestrel[0]
      Unexpected exception in HttpConnection.ProcessRequestsAsync.
      System.Net.Quic.QuicException: An internal error has occurred. StreamShutdown failed: QUIC_STATUS_INVALID_PARAMETER
         at System.Net.Quic.ThrowHelper.ThrowMsQuicException(Int32 status, String message)
         at System.Net.Quic.QuicStream.Abort(QuicAbortDirection abortDirection, Int64 errorCode)
         at Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal.QuicStreamContext.Abort(ConnectionAbortedException abortReason)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3FrameWriter.Abort(ConnectionAbortedException error)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3ControlStream.Abort(ConnectionAbortedException abortReason, Http3ErrorCode errorCode)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3Connection.ProcessRequestsAsync[TContext](IHttpApplication`1 application)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.HttpConnection.ProcessRequestsAsync[TContext](IHttpApplication`1 httpApplication)

```
author_association: NONE
user: login: ManickaP
created_at: 2025-04-04T08:07:32Z
body: The new exception actually looks like https://github.com/dotnet/aspnetcore/issues/45105, I root caused it in https://github.com/dotnet/aspnetcore/issues/45105#issuecomment-1563452631. I'd suggest to comment on that aspnet issue that you're experiencing it as well.
author_association: MEMBER

Diff:
diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs
index c53fb1b0741425..f21521eabe5ccd 100644
--- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs
+++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs
@@ -52,7 +52,7 @@ public ResettableValueTaskSource()
     public Action<object?> CancellationAction { init { _cancellationAction = value; } }
 
     /// <summary>
-    /// Returns <c>true</c> is this task source has entered its final state, i.e. <see cref="TrySetResult(bool)"/> or <see cref="TrySetException(Exception, bool)"/>
+    /// Returns <c>true</c> is this task source has entered its final state, i.e. <see cref="TrySetResult(bool)"/> or <see cref="TrySetException(Exception)"/>
     /// was called with <c>final</c> set to <c>true</c> and the result was propagated.
     /// </summary>
     public bool IsCompleted => (State)Volatile.Read(ref Unsafe.As<State, byte>(ref _state)) == State.Completed;
@@ -173,6 +173,7 @@ private bool TryComplete(Exception? exception, bool final)
                 // Unblock the current task source and in case of a final also the final task source.
                 if (exception is not null)
                 {
+                    Debug.Assert(final);
                     // Set up the exception stack trace for the caller.
                     exception = exception.StackTrace is null ? ExceptionDispatchInfo.SetCurrentStackTrace(exception) : exception;
                     if (state is State.None or State.Awaiting)
@@ -192,7 +193,7 @@ private bool TryComplete(Exception? exception, bool final)
                     if (_finalTaskSource.TryComplete(exception))
                     {
                         // Signal the final task only if we don't have another result in the value task source.
-                        // In that case, the final task will be signalled after the value task result is retrieved.
+                        // In that case, the final task will be signaled after the value task result is retrieved.
                         if (state != State.Ready)
                         {
                             _finalTaskSource.TrySignal(out _);
@@ -226,15 +227,14 @@ public bool TrySetResult(bool final = false)
     }
 
     /// <summary>
-    /// Tries to transition from <see cref="State.Awaiting"/> to either <see cref="State.Ready"/> or <see cref="State.Completed"/>, depending on the value of <paramref name="final"/>.
-    /// Only the first call is able to do that with the exception of <c>TrySetResult()</c> followed by <c>TrySetResult(true)</c>, which will both return <c>true</c>.
+    /// Tries to transition from <see cref="State.Awaiting"/> to <see cref="State.Completed"/>, setting an exception.
+    /// Only the first call is able to do that.
     /// </summary>
-    /// <param name="final">Whether this is the final transition to <see cref="State.Completed" /> or just a transition into <see cref="State.Ready"/> from which the task source can be reset back to <see cref="State.None"/>.</param>
     /// <param name="exception">The exception to set as a result of the value task.</param>
     /// <returns><c>true</c> if this is the first call that set the result; otherwise, <c>false</c>.</returns>
-    public bool TrySetException(Exception exception, bool final = false)
+    public bool TrySetException(Exception exception)
     {
-        return TryComplete(exception, final);
+        return TryComplete(exception, final: true);
     }
 
     ValueTaskSourceStatus IValueTaskSource.GetStatus(short token)
@@ -297,6 +297,7 @@ private struct FinalTaskSource
         private bool _isCompleted;
         private bool _isSignaled;
         private Exception? _exception;
+        private Task? _signaledTask;
 
         public FinalTaskSource()
         {
@@ -312,9 +313,9 @@ public Task GetTask(object? keepAlive)
             {
                 if (_isSignaled)
                 {
-                    return _exception is null
-                        ? Task.CompletedTask
-                        : Task.FromException(_exception);
+                    _signaledTask ??= _exception is null ? Task.CompletedTask : Task.FromException(_exception);
+                    _ = _signaledTask.Exception; // Observe the exception.
+                    return _signaledTask;
                 }
 
                 _finalTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -350,13 +351,17 @@ public bool TrySignal(out Exception? exception)
                 return false;
             }
 
-            if (_exception is not null)
+            if (_finalTaskSource is not null)
             {
-                _finalTaskSource?.SetException(_exception);
-            }
-            else
-            {
-                _finalTaskSource?.SetResult();
+                if (_exception is not null)
+                {
+                    _finalTaskSource.SetException(_exception);
+                    _ = _finalTaskSource.Task.Exception; // Observe the exception.
+                }
+                else
+                {
+                    _finalTaskSource.SetResult();
+                }
             }
 
             exception = _exception;
diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs
index ce0128e779e9ac..04e4bb94f30bf6 100644
--- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs
+++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs
@@ -430,7 +430,7 @@ public ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, bool completeWrites, Ca
                     exception = Volatile.Read(ref _sendException);
                     if (exception is not null)
                     {
-                        _sendTcs.TrySetException(exception, final: true);
+                        _sendTcs.TrySetException(exception);
                     }
                 }
                 // SEND_COMPLETE expected, buffer and lock will be released then.
@@ -487,7 +487,7 @@ public void Abort(QuicAbortDirection abortDirection, long errorCode)
 
         if (abortDirection.HasFlag(QuicAbortDirection.Read))
         {
-            _receiveTcs.TrySetException(ThrowHelper.GetOperationAbortedException(SR.net_quic_reading_aborted), final: true);
+            _receiveTcs.TrySetException(ThrowHelper.GetOperationAbortedException(SR.net_quic_reading_aborted));
         }
         if (abortDirection.HasFlag(QuicAbortDirection.Write))
         {
@@ -495,7 +495,7 @@ public void Abort(QuicAbortDirection abortDirection, long errorCode)
             Interlocked.CompareExchange(ref _sendException, exception, null);
             if (Interlocked.CompareExchange(ref _sendLocked, 1, 0) == 0)
             {
-                _sendTcs.TrySetException(_sendException, final: true);
+                _sendTcs.TrySetException(_sendException);
                 Volatile.Write(ref _sendLocked, 0);
             }
         }
@@ -585,7 +585,7 @@ private unsafe int HandleEventSendComplete(ref SEND_COMPLETE_DATA data)
         Exception? exception = Volatile.Read(ref _sendException);
         if (exception is not null)
         {
-            _sendTcs.TrySetException(exception, final: true);
+            _sendTcs.TrySetException(exception);
         }
         if (data.Canceled == 0)
         {
@@ -604,12 +604,12 @@ private unsafe int HandleEventPeerSendShutdown()
     }
     private unsafe int HandleEventPeerSendAborted(ref PEER_SEND_ABORTED_DATA data)
     {
-        _receiveTcs.TrySetException(ThrowHelper.GetStreamAbortedException((long)data.ErrorCode), final: true);
+        _receiveTcs.TrySetException(ThrowHelper.GetStreamAbortedException((long)data.ErrorCode));
         return QUIC_STATUS_SUCCESS;
     }
     private unsafe int HandleEventPeerReceiveAborted(ref PEER_RECEIVE_ABORTED_DATA data)
     {
-        _sendTcs.TrySetException(ThrowHelper.GetStreamAbortedException((long)data.ErrorCode), final: true);
+        _sendTcs.TrySetException(ThrowHelper.GetStreamAbortedException((long)data.ErrorCode));
         return QUIC_STATUS_SUCCESS;
     }
     private unsafe int HandleEventSendShutdownComplete(ref SEND_SHUTDOWN_COMPLETE_DATA data)
@@ -639,8 +639,8 @@ private unsafe int HandleEventShutdownComplete(ref SHUTDOWN_COMPLETE_DATA data)
                 (shutdownByApp: false, closedRemotely: false) => ThrowHelper.GetExceptionForMsQuicStatus(data.ConnectionCloseStatus, (long)data.ConnectionErrorCode),
             };
             _startedTcs.TrySetException(exception);
-            _receiveTcs.TrySetException(exception, final: true);
-            _sendTcs.TrySetException(exception, final: true);
+            _receiveTcs.TrySetException(exception);
+            _sendTcs.TrySetException(exception);
         }
         _startedTcs.TrySetException(ThrowHelper.GetOperationAbortedException());
         _shutdownTcs.TrySetResult();
@@ -766,11 +766,11 @@ unsafe void StreamShutdown(QUIC_STREAM_SHUTDOWN_FLAGS flags, long errorCode)
             {
                 if (flags.HasFlag(QUIC_STREAM_SHUTDOWN_FLAGS.ABORT_RECEIVE) && !_receiveTcs.IsCompleted)
                 {
-                    _receiveTcs.TrySetException(ThrowHelper.GetOperationAbortedException(SR.net_quic_reading_aborted), final: true);
+                    _receiveTcs.TrySetException(ThrowHelper.GetOperationAbortedException(SR.net_quic_reading_aborted));
                 }
                 if (flags.HasFlag(QUIC_STREAM_SHUTDOWN_FLAGS.ABORT_SEND) && !_sendTcs.IsCompleted)
                 {
-                    _sendTcs.TrySetException(ThrowHelper.GetOperationAbortedException(SR.net_quic_writing_aborted), final: true);
+                    _sendTcs.TrySetException(ThrowHelper.GetOperationAbortedException(SR.net_quic_writing_aborted));
                 }
             }
         }


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR addresses a design concern with exception handling in QUIC streams by ensuring that exceptions are observed and do not leak, while simplifying the API of the ResettableValueTaskSource.  
- Removed the redundant "final" parameter from TrySetException calls in QuicStream.  
- Updated ResettableValueTaskSource to always treat exception completion as final.  
- Added explicit observation of exceptions in the final task to prevent unobserved exceptions.

### Reviewed Changes

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

| File | Description |
| ---- | ----------- |
| src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs | Removed the now-unnecessary "final" parameter in TrySetException calls, simplifying exception propagation. |
| src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs | Refactored TrySetException to always complete in a final state and added explicit observation of exception tasks. |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T17:47:40Z
user: login: ManickaP
body: LGTM, thanks!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-04T07:54:42Z
user: login: antonfirsov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T11:32:43Z

Review comments:
diff_hunk: @@ -430,7 +430,7 @@ public ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, bool completeWrites, Ca
                     exception = Volatile.Read(ref _sendException);
                     if (exception is not null)
                     {
-                        _sendTcs.TrySetException(exception, final: true);
+                        _sendTcs.TrySetException(exception);
path: src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs
user: login: Copilot
body: The removal of the 'final: true' argument simplifies the API in QuicStream; please ensure that all call sites now rely on the default 'final' behavior to correctly complete the task source and avoid unobserved exceptions.
created_at: 2025-04-03T17:47:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -226,15 +227,14 @@ public bool TrySetResult(bool final = false)
     }
 
     /// <summary>
-    /// Tries to transition from <see cref="State.Awaiting"/> to either <see cref="State.Ready"/> or <see cref="State.Completed"/>, depending on the value of <paramref name="final"/>.
-    /// Only the first call is able to do that with the exception of <c>TrySetResult()</c> followed by <c>TrySetResult(true)</c>, which will both return <c>true</c>.
+    /// Tries to transition from <see cref="State.Awaiting"/> to <see cref="State.Completed"/>, setting an exception.
+    /// Only the first call is able to do that.
     /// </summary>
-    /// <param name="final">Whether this is the final transition to <see cref="State.Completed" /> or just a transition into <see cref="State.Ready"/> from which the task source can be reset back to <see cref="State.None"/>.</param>
     /// <param name="exception">The exception to set as a result of the value task.</param>
     /// <returns><c>true</c> if this is the first call that set the result; otherwise, <c>false</c>.</returns>
-    public bool TrySetException(Exception exception, bool final = false)
+    public bool TrySetException(Exception exception)
path: src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs
user: login: Copilot
body: The API change to remove the non-final option in TrySetException simplifies usage; verify that all callers are updated to expect a final completion state when setting exceptions.
created_at: 2025-04-03T17:47:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -312,9 +313,9 @@ public Task GetTask(object? keepAlive)
             {
                 if (_isSignaled)
                 {
-                    return _exception is null
-                        ? Task.CompletedTask
-                        : Task.FromException(_exception);
+                    _signaledTask ??= _exception is null ? Task.CompletedTask : Task.FromException(_exception);
path: src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs
user: login: Copilot
body: The introduction of _signaledTask with an explicit observation of its Exception is a proactive measure to prevent unobserved exception warnings; consider adding an inline comment that clarifies the rationale for future maintainers.
created_at: 2025-04-03T17:47:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -339,6 +340,7 @@ public bool TryComplete(Exception? exception = null)
 
             _exception = exception;
             _isCompleted = true;
+
path: src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs
user: login: ManickaP
body: ```suggestion
```
created_at: 2025-04-04T07:22:42Z
author_association: MEMBER
diff_hunk: @@ -226,15 +227,14 @@ public bool TrySetResult(bool final = false)
     }
 
     /// <summary>
-    /// Tries to transition from <see cref="State.Awaiting"/> to either <see cref="State.Ready"/> or <see cref="State.Completed"/>, depending on the value of <paramref name="final"/>.
-    /// Only the first call is able to do that with the exception of <c>TrySetResult()</c> followed by <c>TrySetResult(true)</c>, which will both return <c>true</c>.
+    /// Tries to transition from <see cref="State.Awaiting"/> to <see cref="State.Completed"/>, setting an exception.
+    /// Only the first call is able to do that.
     /// </summary>
-    /// <param name="final">Whether this is the final transition to <see cref="State.Completed" /> or just a transition into <see cref="State.Ready"/> from which the task source can be reset back to <see cref="State.None"/>.</param>
     /// <param name="exception">The exception to set as a result of the value task.</param>
     /// <returns><c>true</c> if this is the first call that set the result; otherwise, <c>false</c>.</returns>
-    public bool TrySetException(Exception exception, bool final = false)
+    public bool TrySetException(Exception exception)
path: src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs
user: login: ManickaP
body: Just for the explanation: the `bool final = false`  was here to have the "same" signature as `TrySetResult` and in the past we did have non-final exceptions (from cancellation).

It could have also gone the other way by making `final` mandatory to all calls to `TrySetResult` and `TrySetException`.

Anyway, this is not a hill I want to die on, so keep the change as-is if you prefer it this way.
created_at: 2025-04-04T07:54:01Z
author_association: MEMBER
diff_hunk: @@ -226,15 +227,14 @@ public bool TrySetResult(bool final = false)
     }
 
     /// <summary>
-    /// Tries to transition from <see cref="State.Awaiting"/> to either <see cref="State.Ready"/> or <see cref="State.Completed"/>, depending on the value of <paramref name="final"/>.
-    /// Only the first call is able to do that with the exception of <c>TrySetResult()</c> followed by <c>TrySetResult(true)</c>, which will both return <c>true</c>.
+    /// Tries to transition from <see cref="State.Awaiting"/> to <see cref="State.Completed"/>, setting an exception.
+    /// Only the first call is able to do that.
     /// </summary>
-    /// <param name="final">Whether this is the final transition to <see cref="State.Completed" /> or just a transition into <see cref="State.Ready"/> from which the task source can be reset back to <see cref="State.None"/>.</param>
     /// <param name="exception">The exception to set as a result of the value task.</param>
     /// <returns><c>true</c> if this is the first call that set the result; otherwise, <c>false</c>.</returns>
-    public bool TrySetException(Exception exception, bool final = false)
+    public bool TrySetException(Exception exception)
path: src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs
user: login: antonfirsov
body: I don't plan to do further refactors in this PR, just leaving my take: It would be better to make it mandatory on `TrySetResult()` so it's easier to see what the call sites are doing. We can add it back to `TrySetException` when/if it turns out to be needed again, till then I don't see the value.
created_at: 2025-04-04T11:32:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: dotnet-policy-service[bot]
created_at: 2025-04-03T17:47:55Z
body: Tagging subscribers to this area: @dotnet/ncl
See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
<!-- Policy app identification https://img.shields.io/static/v1?label=PullRequestIssueManagement. -->
author_association: CONTRIBUTOR
user: login: antonfirsov
created_at: 2025-04-04T11:40:35Z
body: /azp run runtime-libraries stress-http
author_association: CONTRIBUTOR
user: login: azure-pipelines[bot]
created_at: 2025-04-04T11:40:43Z
body: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
author_association: NONE
user: login: azure-pipelines[bot]
created_at: 2025-04-09T15:10:06Z
body: <samp>
No pipelines are associated with this pull request.<br>

</samp>
author_association: NONE
user: login: antonfirsov
created_at: 2025-04-09T15:11:17Z
body: /azp run runtime-libraries-coreclr outerloop
author_association: CONTRIBUTOR
user: login: azure-pipelines[bot]
created_at: 2025-04-09T15:11:32Z
body: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
author_association: NONE
user: login: antonfirsov
created_at: 2025-04-15T14:10:27Z
body: /ba-g CI failures are unrelated
author_association: CONTRIBUTOR