From 94e576f1ab6a2efce9f5c59c304e2da0e2a01b96 Mon Sep 17 00:00:00 2001 From: ManickaP Date: Thu, 21 May 2026 15:57:57 +0200 Subject: [PATCH 1/2] add test --- .../tests/FunctionalTests/QuicStreamTests.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs index 93d48ec4649fd5..c8f20fbc590a78 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs @@ -786,6 +786,32 @@ await RunBidirectionalClientServer( }); } + [Fact] + public async Task WritesCompleted_WritesAsync_Throws() + { + SemaphoreSlim sem = new SemaphoreSlim(0); + await RunBidirectionalClientServer( + async clientStream => + { + // Close and wait for write completion. + clientStream.CompleteWrites(); + await clientStream.WritesClosed; + + // This should throw. + await clientStream.WriteAsync(new byte[2] {1, 2}, false); + + await sem.WaitAsync(); + }, + async serverStream => + { + int received = await serverStream.ReadAsync(new byte[1]); + Assert.Equal(0, received); + await serverStream.ReadsClosed; + + sem.Release(); + }); + } + [Fact] public async Task WaitForWritesClosedAsync_ServerWriteAborted_Throws() { From 18213f4ef3122e633d072f0b1f56537856dfd01d Mon Sep 17 00:00:00 2001 From: ManickaP Date: Fri, 22 May 2026 16:42:45 +0200 Subject: [PATCH 2/2] Fix #121619 --- src/libraries/System.Net.Quic/src/Resources/Strings.resx | 4 +++- .../System/Net/Quic/Internal/ResettableValueTaskSource.cs | 2 +- .../src/System/Net/Quic/Internal/ValueTaskSource.cs | 4 ++-- .../System.Net.Quic/src/System/Net/Quic/QuicStream.cs | 6 +++++- .../tests/FunctionalTests/QuicStreamTests.cs | 5 +++-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Net.Quic/src/Resources/Strings.resx b/src/libraries/System.Net.Quic/src/Resources/Strings.resx index f89a953d8d6242..53114ad08ee162 100644 --- a/src/libraries/System.Net.Quic/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Quic/src/Resources/Strings.resx @@ -226,6 +226,9 @@ Authentication failed because the remote party sent a TLS alert: '{0}'. + + This method may not be called when writing side was already completed. + The AddressFamily {0} is not valid for the {1} end point, use {2} instead. @@ -240,4 +243,3 @@ Authentication failed because the platform does not support ephemeral keys. - 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 f21521eabe5ccd..786b6594840a64 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 CancellationAction { init { _cancellationAction = value; } } /// - /// Returns true is this task source has entered its final state, i.e. or + /// Returns true if this task source has entered its final state, i.e. or /// was called with final set to true and the result was propagated. /// public bool IsCompleted => (State)Volatile.Read(ref Unsafe.As(ref _state)) == State.Completed; diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs index c44d08b310051f..359be029e22c9f 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs @@ -36,11 +36,11 @@ public ValueTaskSource() } /// - /// Returns true is this task source was completed, i.e. or was called. + /// Returns true if this task source was completed, i.e. or was called. /// public bool IsCompleted => (State)Volatile.Read(ref Unsafe.As(ref _state)) == State.Completed; /// - /// Returns true is this task source was completed successfully, i.e. was called and set the result. + /// Returns true if this task source was completed successfully, i.e. was called and set the result. /// public bool IsCompletedSuccessfully => IsCompleted && _valueTaskSource.GetStatus(_valueTaskSource.Version) == ValueTaskSourceStatus.Succeeded; 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 133cd1aa7030a6..15b767e7937884 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 @@ -420,7 +420,11 @@ public ValueTask WriteAsync(ReadOnlyMemory buffer, bool completeWrites, Ca // No need to call anything since we already have a result, most likely an exception. if (valueTask.IsCompleted) { - return valueTask; + // It doesn't matter that we throw away the valueTask here, it doesn't need to be reset anymore. + // The writing side is closed, the task is completed, and it will never get past this condition. + return valueTask.IsCompletedSuccessfully ? + ValueTask.FromException(ExceptionDispatchInfo.SetCurrentStackTrace(new InvalidOperationException(SR.net_writecompleted_invalidcall))) : + valueTask; } // For an empty buffer complete immediately, close the writing side of the stream if necessary. diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs index c8f20fbc590a78..9dacfb307b275e 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs @@ -797,8 +797,9 @@ await RunBidirectionalClientServer( clientStream.CompleteWrites(); await clientStream.WritesClosed; - // This should throw. - await clientStream.WriteAsync(new byte[2] {1, 2}, false); + // These both should throw the same exception. + await Assert.ThrowsAsync(async () => await clientStream.WriteAsync(new byte[0], false)); + await Assert.ThrowsAsync(async () => await clientStream.WriteAsync(new byte[0], false)); await sem.WaitAsync(); },