Skip to content

Commit d99627b

Browse files
committed
HTTP/3: Add IStreamAbortFeature
1 parent 5753baf commit d99627b

File tree

6 files changed

+80
-11
lines changed

6 files changed

+80
-11
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Connections.Features
5+
{
6+
/// <summary>
7+
/// Explicitly abort one direction of a connection stream.
8+
/// </summary>
9+
public interface IStreamAbortFeature
10+
{
11+
/// <summary>
12+
/// Abort reading from the connection stream.
13+
/// </summary>
14+
/// <param name="abortReason">An optional <see cref="ConnectionAbortedException"/> describing the reason to abort reading from the connection stream.</param>
15+
void AbortRead(ConnectionAbortedException abortReason);
16+
17+
/// <summary>
18+
/// Abort writing to the connection stream.
19+
/// </summary>
20+
/// <param name="abortReason">An optional <see cref="ConnectionAbortedException"/> describing the reason to abort writing to the connection stream.</param>
21+
void AbortWrite(ConnectionAbortedException abortReason);
22+
}
23+
}

src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
*REMOVED*Microsoft.AspNetCore.Connections.IConnectionListener.AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext!>
33
Microsoft.AspNetCore.Connections.Features.IConnectionSocketFeature
44
Microsoft.AspNetCore.Connections.Features.IConnectionSocketFeature.Socket.get -> System.Net.Sockets.Socket!
5+
Microsoft.AspNetCore.Connections.Features.IStreamAbortFeature
6+
Microsoft.AspNetCore.Connections.Features.IStreamAbortFeature.AbortRead(Microsoft.AspNetCore.Connections.ConnectionAbortedException! abortReason) -> void
7+
Microsoft.AspNetCore.Connections.Features.IStreamAbortFeature.AbortWrite(Microsoft.AspNetCore.Connections.ConnectionAbortedException! abortReason) -> void
58
Microsoft.AspNetCore.Connections.IConnectionListener.AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext?>
69
Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder
710
Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder.ApplicationServices.get -> System.IServiceProvider!

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpH
4242
private readonly Http3StreamContext _context;
4343
private readonly IProtocolErrorCodeFeature _errorCodeFeature;
4444
private readonly IStreamIdFeature _streamIdFeature;
45+
private readonly IStreamAbortFeature _streamAbortFeature;
4546
private readonly Http3RawFrame _incomingFrame = new Http3RawFrame();
4647
protected RequestHeaderParsingState _requestHeaderParsingState;
4748
private PseudoHeaderFields _parsedPseudoHeaderFields;
@@ -69,6 +70,7 @@ public Http3Stream(Http3StreamContext context)
6970

7071
_errorCodeFeature = _context.ConnectionFeatures.Get<IProtocolErrorCodeFeature>()!;
7172
_streamIdFeature = _context.ConnectionFeatures.Get<IStreamIdFeature>()!;
73+
_streamAbortFeature = _context.ConnectionFeatures.Get<IStreamAbortFeature>()!;
7274

7375
_frameWriter = new Http3FrameWriter(
7476
context.Transport.Output,
@@ -353,10 +355,8 @@ private void CompleteStream(bool errored)
353355
// the request stream, send a complete response, and cleanly close the sending part of the stream.
354356
// The error code H3_NO_ERROR SHOULD be used when requesting that the client stop sending on the
355357
// request stream.
356-
357-
// TODO(JamesNK): Abort the read half of the stream with H3_NO_ERROR
358-
// https://github.com/dotnet/aspnetcore/issues/33575
359-
358+
_errorCodeFeature.Error = (long)Http3ErrorCode.NoError;
359+
_streamAbortFeature.AbortRead(new ConnectionAbortedException("The application completed without reading the entire request body."));
360360
RequestBodyPipe.Writer.Complete();
361361
}
362362

src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
1616
{
17-
internal class QuicStreamContext : TransportConnection, IStreamDirectionFeature, IProtocolErrorCodeFeature, IStreamIdFeature
17+
internal class QuicStreamContext : TransportConnection, IStreamDirectionFeature, IProtocolErrorCodeFeature, IStreamIdFeature, IStreamAbortFeature
1818
{
1919
// Internal for testing.
2020
internal Task _processingTask = Task.CompletedTask;
@@ -312,6 +312,36 @@ public override void Abort(ConnectionAbortedException abortReason)
312312
Output.CancelPendingRead();
313313
}
314314

315+
public void AbortRead(ConnectionAbortedException abortReason)
316+
{
317+
lock (_shutdownLock)
318+
{
319+
if (_stream.CanRead)
320+
{
321+
_stream.AbortRead(Error);
322+
}
323+
else
324+
{
325+
throw new InvalidOperationException("Unable to abort reading from a stream that doesn't support reading.");
326+
}
327+
}
328+
}
329+
330+
public void AbortWrite(ConnectionAbortedException abortReason)
331+
{
332+
lock (_shutdownLock)
333+
{
334+
if (_stream.CanWrite)
335+
{
336+
_stream.AbortWrite(Error);
337+
}
338+
else
339+
{
340+
throw new InvalidOperationException("Unable to abort writing to a stream that doesn't support writing.");
341+
}
342+
}
343+
}
344+
315345
private async ValueTask ShutdownWrite(Exception? shutdownReason)
316346
{
317347
try

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2431,8 +2431,8 @@ public async Task MaxRequestBodySize_ContentLengthOver_413()
24312431

24322432
await requestStream.OnStreamCompletedTask.DefaultTimeout();
24332433

2434-
// TODO(JamesNK): Check for abort of request side of stream after https://github.com/dotnet/aspnetcore/issues/31970
24352434
Assert.Contains(LogMessages, m => m.Message.Contains("the application completed without reading the entire request body."));
2435+
Assert.Equal("The application completed without reading the entire request body.", requestStream.AbortReadException.Message);
24362436

24372437
Assert.Equal(3, receivedHeaders.Count);
24382438
Assert.Contains("date", receivedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
@@ -2756,8 +2756,8 @@ await requestStream.SendHeadersAsync(new[]
27562756

27572757
await requestStream.OnStreamCompletedTask.DefaultTimeout();
27582758

2759-
// TODO(JamesNK): Check for abort of request side of stream after https://github.com/dotnet/aspnetcore/issues/31970
27602759
Assert.Contains(LogMessages, m => m.Message.Contains("the application completed without reading the entire request body."));
2760+
Assert.Equal("The application completed without reading the entire request body.", requestStream.AbortReadException.Message);
27612761
}
27622762

27632763
[Fact]

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ public ValueTask<ConnectionContext> StartBidirectionalStreamAsync()
466466
return new ValueTask<ConnectionContext>(stream.StreamContext);
467467
}
468468

469-
public class Http3StreamBase : IProtocolErrorCodeFeature
469+
public class Http3StreamBase : IProtocolErrorCodeFeature, IStreamAbortFeature
470470
{
471471
internal TaskCompletionSource _onStreamCreatedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
472472
internal TaskCompletionSource _onStreamCompletedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -477,11 +477,23 @@ public class Http3StreamBase : IProtocolErrorCodeFeature
477477
internal Http3Connection _connection;
478478
public long BytesReceived { get; private set; }
479479
public long Error { get; set; }
480+
public ConnectionAbortedException AbortReadException { get; private set; }
481+
public ConnectionAbortedException AbortWriteException { get; private set; }
480482

481483
public Task OnStreamCreatedTask => _onStreamCreatedTcs.Task;
482484
public Task OnStreamCompletedTask => _onStreamCompletedTcs.Task;
483485
public Task OnHeaderReceivedTask => _onHeaderReceivedTcs.Task;
484486

487+
void IStreamAbortFeature.AbortRead(ConnectionAbortedException abortReason)
488+
{
489+
AbortReadException = abortReason;
490+
}
491+
492+
void IStreamAbortFeature.AbortWrite(ConnectionAbortedException abortReason)
493+
{
494+
AbortWriteException = abortReason;
495+
}
496+
485497
protected Task SendAsync(ReadOnlySpan<byte> span)
486498
{
487499
var writableBuffer = _pair.Application.Output;
@@ -610,7 +622,7 @@ public Http3RequestStream(Http3TestBase testBase, Http3Connection connection)
610622

611623
_pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
612624
_streamId = testBase.GetStreamId(0x00);
613-
_testStreamContext = new TestStreamContext(canRead: true, canWrite: true, _pair, this, _streamId);
625+
_testStreamContext = new TestStreamContext(canRead: true, canWrite: true, _pair, this, this, _streamId);
614626
StreamContext = _testStreamContext;
615627
}
616628

@@ -736,7 +748,7 @@ public Http3ControlStream(Http3TestBase testBase, StreamInitiator initiator)
736748
var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool);
737749
_pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
738750
_streamId = testBase.GetStreamId(initiator == StreamInitiator.Client ? 0x02 : 0x03);
739-
StreamContext = new TestStreamContext(canRead: true, canWrite: false, _pair, this, _streamId);
751+
StreamContext = new TestStreamContext(canRead: true, canWrite: false, _pair, this, this, _streamId);
740752
}
741753

742754
public Http3ControlStream(ConnectionContext streamContext)
@@ -957,12 +969,13 @@ public void RequestClose()
957969
private class TestStreamContext : ConnectionContext, IStreamDirectionFeature, IStreamIdFeature
958970
{
959971
private readonly DuplexPipePair _pair;
960-
public TestStreamContext(bool canRead, bool canWrite, DuplexPipePair pair, IProtocolErrorCodeFeature errorCodeFeature, long streamId)
972+
public TestStreamContext(bool canRead, bool canWrite, DuplexPipePair pair, IProtocolErrorCodeFeature errorCodeFeature, IStreamAbortFeature streamAbortFeature, long streamId)
961973
{
962974
_pair = pair;
963975
Features = new FeatureCollection();
964976
Features.Set<IStreamDirectionFeature>(this);
965977
Features.Set<IStreamIdFeature>(this);
978+
Features.Set(streamAbortFeature);
966979
Features.Set(errorCodeFeature);
967980

968981
CanRead = canRead;

0 commit comments

Comments
 (0)