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()
{