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 93d48ec4649fd5..9dacfb307b275e 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs @@ -786,6 +786,33 @@ 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; + + // 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(); + }, + 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() {