Skip to content

Commit 5e1244e

Browse files
authored
HTTP/3: Support canceling requests that aren't reading a body (dotnet#36017)
1 parent 5eae5cf commit 5e1244e

17 files changed

+453
-88
lines changed

Diff for: src/Servers/Connections.Abstractions/src/Features/IProtocolErrorCodeFeature.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Connections.Features
99
public interface IProtocolErrorCodeFeature
1010
{
1111
/// <summary>
12-
/// Gets or sets the error code.
12+
/// Gets or sets the error code. The property returns -1 if the error code hasn't been set.
1313
/// </summary>
1414
long Error { get; set; }
1515
}

Diff for: src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

+60-19
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ public void InitializeWithExistingContext(IDuplexPipe transport)
133133
}
134134

135135
public void Abort(ConnectionAbortedException abortReason, Http3ErrorCode errorCode)
136+
{
137+
AbortCore(abortReason, errorCode);
138+
}
139+
140+
private void AbortCore(Exception exception, Http3ErrorCode errorCode)
136141
{
137142
lock (_completionLock)
138143
{
@@ -148,26 +153,27 @@ public void Abort(ConnectionAbortedException abortReason, Http3ErrorCode errorCo
148153
return;
149154
}
150155

151-
Log.Http3StreamAbort(TraceIdentifier, errorCode, abortReason);
156+
if (!(exception is ConnectionAbortedException abortReason))
157+
{
158+
abortReason = new ConnectionAbortedException(exception.Message, exception);
159+
}
152160

153-
_errorCodeFeature.Error = (long)errorCode;
154-
_frameWriter.Abort(abortReason);
161+
Log.Http3StreamAbort(TraceIdentifier, errorCode, abortReason);
155162

156-
AbortCore(abortReason);
157-
}
158-
}
163+
// Call _http3Output.Stop() prior to poisoning the request body stream or pipe to
164+
// ensure that an app that completes early due to the abort doesn't result in header frames being sent.
165+
_http3Output.Stop();
159166

160-
private void AbortCore(Exception exception)
161-
{
162-
// Call _http3Output.Stop() prior to poisoning the request body stream or pipe to
163-
// ensure that an app that completes early due to the abort doesn't result in header frames being sent.
164-
_http3Output.Stop();
167+
CancelRequestAbortedToken();
165168

166-
CancelRequestAbortedToken();
169+
// Unblock the request body.
170+
PoisonBody(exception);
171+
RequestBodyPipe.Writer.Complete(exception);
167172

168-
// Unblock the request body.
169-
PoisonBody(exception);
170-
RequestBodyPipe.Writer.Complete(exception);
173+
// Abort framewriter and underlying transport after stopping output.
174+
_errorCodeFeature.Error = (long)errorCode;
175+
_frameWriter.Abort(abortReason);
176+
}
171177
}
172178

173179
protected override void OnErrorAfterResponseStarted()
@@ -470,7 +476,8 @@ public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> appli
470476
catch (ConnectionResetException ex)
471477
{
472478
error = ex;
473-
AbortCore(new IOException(CoreStrings.HttpStreamResetByClient, ex));
479+
var resolvedErrorCode = _errorCodeFeature.Error >= 0 ? _errorCodeFeature.Error : 0;
480+
AbortCore(new IOException(CoreStrings.HttpStreamResetByClient, ex), (Http3ErrorCode)resolvedErrorCode);
474481
}
475482
catch (Exception ex)
476483
{
@@ -484,10 +491,44 @@ public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> appli
484491

485492
await Input.CompleteAsync();
486493

487-
// Make sure application func is completed before completing writer.
488-
if (_appCompleted != null)
494+
var appCompleted = _appCompleted?.Task ?? Task.CompletedTask;
495+
if (!appCompleted.IsCompletedSuccessfully)
489496
{
490-
await _appCompleted.Task;
497+
// At this point in the stream's read-side is complete. However, with HTTP/3
498+
// the write-side of the stream can still be aborted by the client on request
499+
// aborted.
500+
//
501+
// To get notification of request aborted we register to connection closed
502+
// token. It will notify this type that the client has aborted the request
503+
// and Kestrel will complete pipes and cancel the RequestAborted token.
504+
//
505+
// Only subscribe to this event after the stream's read-side is complete to
506+
// avoid interactions between reading that is in-progress and an abort.
507+
// This means while reading, read-side abort will handle getting abort notifications.
508+
//
509+
// We don't need to hang on to the CancellationTokenRegistration from register.
510+
// The CTS is cleaned up in StreamContext.DisposeAsync.
511+
//
512+
// TODO: Consider a better way to provide this notification. For perf we want to
513+
// make the ConnectionClosed CTS pay-for-play, and change this event to use
514+
// something that is more lightweight than a CTS.
515+
_context.StreamContext.ConnectionClosed.Register(static s =>
516+
{
517+
var stream = (Http3Stream)s!;
518+
519+
if (!stream.IsCompleted)
520+
{
521+
// An error code value other than -1 indicates a value was set and the request didn't gracefully complete.
522+
var errorCode = stream._errorCodeFeature.Error;
523+
if (errorCode >= 0)
524+
{
525+
stream.AbortCore(new IOException(CoreStrings.HttpStreamResetByClient), (Http3ErrorCode)errorCode);
526+
}
527+
}
528+
}, this);
529+
530+
// Make sure application func is completed before completing writer.
531+
await appCompleted;
491532
}
492533

493534
try

Diff for: src/Servers/Kestrel/Transport.Quic/src/Internal/IQuicTrace.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ internal interface IQuicTrace : ILogger
1919
void StreamPause(QuicStreamContext streamContext);
2020
void StreamResume(QuicStreamContext streamContext);
2121
void StreamShutdownWrite(QuicStreamContext streamContext, string reason);
22-
void StreamAborted(QuicStreamContext streamContext, long errorCode, Exception ex);
22+
void StreamAbortedRead(QuicStreamContext streamContext, long errorCode);
23+
void StreamAbortedWrite(QuicStreamContext streamContext, long errorCode);
2324
void StreamAbort(QuicStreamContext streamContext, long errorCode, string reason);
2425
void StreamAbortRead(QuicStreamContext streamContext, long errorCode, string reason);
2526
void StreamAbortWrite(QuicStreamContext streamContext, long errorCode, string reason);

Diff for: src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.FeatureCollection.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ internal sealed partial class QuicConnectionContext : IProtocolErrorCodeFeature,
1616
{
1717
private X509Certificate2? _clientCert;
1818
private Task<X509Certificate2?>? _clientCertTask;
19+
private long? _error;
1920

20-
public long Error { get; set; }
21+
public long Error
22+
{
23+
get => _error ?? -1;
24+
set => _error = value;
25+
}
2126

2227
public X509Certificate2? ClientCertificate
2328
{

Diff for: src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ public override void Abort(ConnectionAbortedException abortReason)
8383
return;
8484
}
8585

86+
var resolvedErrorCode = _error ?? 0;
8687
_abortReason = ExceptionDispatchInfo.Capture(abortReason);
87-
_log.ConnectionAbort(this, Error, abortReason.Message);
88-
_closeTask = _connection.CloseAsync(errorCode: Error).AsTask();
88+
_log.ConnectionAbort(this, resolvedErrorCode, abortReason.Message);
89+
_closeTask = _connection.CloseAsync(errorCode: resolvedErrorCode).AsTask();
8990
}
9091
}
9192

@@ -127,7 +128,7 @@ public override void Abort(ConnectionAbortedException abortReason)
127128
catch (QuicConnectionAbortedException ex)
128129
{
129130
// Shutdown initiated by peer, abortive.
130-
Error = ex.ErrorCode;
131+
_error = ex.ErrorCode;
131132
_log.ConnectionAborted(this, ex.ErrorCode, ex);
132133

133134
ThreadPool.UnsafeQueueUserWorkItem(state =>

Diff for: src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
99
internal sealed partial class QuicStreamContext : IPersistentStateFeature, IStreamDirectionFeature, IProtocolErrorCodeFeature, IStreamIdFeature, IStreamAbortFeature
1010
{
1111
private IDictionary<object, object?>? _persistentState;
12+
private long? _error;
1213

1314
public bool CanRead { get; private set; }
1415
public bool CanWrite { get; private set; }
1516

16-
public long Error { get; set; }
17+
public long Error
18+
{
19+
get => _error ?? -1;
20+
set => _error = value;
21+
}
1722

1823
public long StreamId { get; private set; }
1924

0 commit comments

Comments
 (0)