Skip to content

Commit 21db906

Browse files
committed
Add tests verifying that that HTTP/3 streams do not get reused after they are aborted
1 parent 0f999f2 commit 21db906

File tree

1 file changed

+154
-0
lines changed

1 file changed

+154
-0
lines changed

src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,160 @@ public async Task GET_MultipleRequestsInSequence_ReusedState()
879879
}
880880
}
881881

882+
[ConditionalFact]
883+
[MsQuicSupported]
884+
public async Task GET_RequestAbortedByClient_StateNotReused()
885+
{
886+
// Arrange
887+
object persistedState = null;
888+
var requestCount = 0;
889+
var abortedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
890+
var requestStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
891+
892+
var builder = CreateHostBuilder(async context =>
893+
{
894+
requestCount++;
895+
var persistentStateCollection = context.Features.Get<IPersistentStateFeature>().State;
896+
if (persistentStateCollection.TryGetValue("Counter", out var value))
897+
{
898+
persistedState = value;
899+
}
900+
persistentStateCollection["Counter"] = requestCount;
901+
902+
if (requestCount == 1)
903+
{
904+
// For the first request, wait for RequestAborted to fire before returning
905+
context.RequestAborted.Register(() =>
906+
{
907+
Logger.LogInformation("Server received cancellation");
908+
abortedTcs.SetResult();
909+
});
910+
911+
// Signal that the request has started and is ready to be cancelled
912+
requestStartedTcs.SetResult();
913+
914+
// Wait for the request to be aborted
915+
await abortedTcs.Task;
916+
}
917+
});
918+
919+
using (var host = builder.Build())
920+
using (var client = HttpHelpers.CreateClient())
921+
{
922+
await host.StartAsync();
923+
924+
// Act - Send first request and cancel it
925+
var cts1 = new CancellationTokenSource();
926+
var request1 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/");
927+
request1.Version = HttpVersion.Version30;
928+
request1.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
929+
930+
var responseTask1 = client.SendAsync(request1, cts1.Token);
931+
932+
// Wait for the server to start processing the request
933+
await requestStartedTcs.Task.DefaultTimeout();
934+
935+
// Cancel the first request
936+
cts1.Cancel();
937+
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => responseTask1).DefaultTimeout();
938+
939+
// Wait for the server to process the abort
940+
await abortedTcs.Task.DefaultTimeout();
941+
942+
// Store the state from the first (aborted) request
943+
var firstRequestState = persistedState;
944+
945+
// Delay to ensure the stream has enough time to return to pool
946+
await Task.Delay(100);
947+
948+
// Send second request (should not reuse state from aborted request)
949+
var request2 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/");
950+
request2.Version = HttpVersion.Version30;
951+
request2.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
952+
953+
var response2 = await client.SendAsync(request2, CancellationToken.None);
954+
response2.EnsureSuccessStatusCode();
955+
var secondRequestState = persistedState;
956+
957+
// Assert
958+
// First request has no persisted state (it was aborted)
959+
Assert.Null(firstRequestState);
960+
961+
// Second request should also have no persisted state since the first request was aborted
962+
// and state should not be reused from aborted requests
963+
Assert.Null(secondRequestState);
964+
965+
await host.StopAsync();
966+
}
967+
}
968+
969+
[ConditionalFact]
970+
[MsQuicSupported]
971+
public async Task GET_RequestAbortedByServer_StateNotReused()
972+
{
973+
// Arrange
974+
object persistedState = null;
975+
var requestCount = 0;
976+
977+
var builder = CreateHostBuilder(context =>
978+
{
979+
requestCount++;
980+
var persistentStateCollection = context.Features.Get<IPersistentStateFeature>().State;
981+
if (persistentStateCollection.TryGetValue("Counter", out var value))
982+
{
983+
persistedState = value;
984+
}
985+
persistentStateCollection["Counter"] = requestCount;
986+
987+
if (requestCount == 1)
988+
{
989+
context.Abort();
990+
}
991+
992+
return Task.CompletedTask;
993+
});
994+
995+
using (var host = builder.Build())
996+
using (var client = HttpHelpers.CreateClient())
997+
{
998+
await host.StartAsync();
999+
1000+
var request1 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/");
1001+
request1.Version = HttpVersion.Version30;
1002+
request1.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
1003+
1004+
var responseTask1 = client.SendAsync(request1, CancellationToken.None);
1005+
var ex = await Assert.ThrowsAnyAsync<HttpRequestException>(() => responseTask1).DefaultTimeout();
1006+
var innerEx = Assert.IsType<HttpProtocolException>(ex.InnerException);
1007+
Assert.Equal(Http3ErrorCode.InternalError, (Http3ErrorCode)innerEx.ErrorCode);
1008+
1009+
// Store the state from the first (aborted) request
1010+
var firstRequestState = persistedState;
1011+
1012+
// Delay to ensure the stream has enough time to return to pool
1013+
await Task.Delay(100);
1014+
1015+
// Send second request (should not reuse state from aborted request)
1016+
var request2 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/");
1017+
request2.Version = HttpVersion.Version30;
1018+
request2.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
1019+
1020+
var response2 = await client.SendAsync(request2, CancellationToken.None);
1021+
response2.EnsureSuccessStatusCode();
1022+
var secondRequestState = persistedState;
1023+
1024+
// Assert
1025+
// First request has no persisted state (it was aborted)
1026+
Assert.Null(firstRequestState);
1027+
1028+
// Second request should also have no persisted state since the first request was aborted
1029+
// and state should not be reused from aborted requests
1030+
Assert.Null(secondRequestState);
1031+
1032+
await host.StopAsync();
1033+
}
1034+
}
1035+
8821036
[ConditionalFact]
8831037
[MsQuicSupported]
8841038
public async Task GET_MultipleRequests_RequestVersionOrHigher_UpgradeToHttp3()

0 commit comments

Comments
 (0)