3
3
4
4
using System . Net ;
5
5
using System . Net . Http ;
6
+ using System . Net . Http . Headers ;
7
+ using System . Net . Quic ;
6
8
using System . Text ;
7
9
using Microsoft . AspNetCore . Builder ;
8
10
using Microsoft . AspNetCore . Connections ;
9
11
using Microsoft . AspNetCore . Connections . Features ;
10
12
using Microsoft . AspNetCore . Hosting ;
11
13
using Microsoft . AspNetCore . Http ;
14
+ using Microsoft . AspNetCore . Internal ;
12
15
using Microsoft . AspNetCore . Server . Kestrel . Core ;
13
16
using Microsoft . AspNetCore . Server . Kestrel . FunctionalTests ;
14
17
using Microsoft . AspNetCore . Testing ;
@@ -24,11 +27,19 @@ private class StreamingHttpContext : HttpContent
24
27
private readonly TaskCompletionSource _completeTcs = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
25
28
private readonly TaskCompletionSource < Stream > _getStreamTcs = new TaskCompletionSource < Stream > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
26
29
27
- protected override async Task SerializeToStreamAsync ( Stream stream , TransportContext context )
30
+ protected override Task SerializeToStreamAsync ( Stream stream , TransportContext context )
31
+ {
32
+ throw new NotSupportedException ( ) ;
33
+ }
34
+
35
+ protected override async Task SerializeToStreamAsync ( Stream stream , TransportContext context , CancellationToken cancellationToken )
28
36
{
29
37
_getStreamTcs . TrySetResult ( stream ) ;
30
38
31
- await _completeTcs . Task ;
39
+ var cancellationTcs = new TaskCompletionSource ( ) ;
40
+ cancellationToken . Register ( ( ) => cancellationTcs . TrySetCanceled ( ) ) ;
41
+
42
+ await Task . WhenAny ( _completeTcs . Task , cancellationTcs . Task ) ;
32
43
}
33
44
34
45
protected override bool TryComputeLength ( out long length )
@@ -52,10 +63,10 @@ public void CompleteStream()
52
63
53
64
[ ConditionalFact ]
54
65
[ MsQuicSupported ]
55
- public async Task POST_ServerCompletsWithoutReadingRequestBody_ClientGetsResponse ( )
66
+ public async Task POST_ServerCompletesWithoutReadingRequestBody_ClientGetsResponse ( )
56
67
{
57
68
// Arrange
58
- var builder = CreateHttp3HostBuilder ( async context =>
69
+ var builder = CreateHostBuilder ( async context =>
59
70
{
60
71
var body = context . Request . Body ;
61
72
@@ -108,6 +119,168 @@ public async Task POST_ServerCompletsWithoutReadingRequestBody_ClientGetsRespons
108
119
}
109
120
}
110
121
122
+ // Verify HTTP/2 and HTTP/3 match behavior
123
+ [ ConditionalTheory ]
124
+ [ MsQuicSupported ]
125
+ [ InlineData ( HttpProtocols . Http3 ) ]
126
+ [ InlineData ( HttpProtocols . Http2 ) ]
127
+ public async Task POST_ClientCancellationUpload_RequestAbortRaised ( HttpProtocols protocol )
128
+ {
129
+ // Arrange
130
+ var syncPoint = new SyncPoint ( ) ;
131
+ var cancelledTcs = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
132
+ var readAsyncTask = new TaskCompletionSource < Task > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
133
+
134
+ var builder = CreateHostBuilder ( async context =>
135
+ {
136
+ context . RequestAborted . Register ( ( ) => cancelledTcs . SetResult ( ) ) ;
137
+
138
+ var body = context . Request . Body ;
139
+
140
+ // Read content
141
+ var data = new List < byte > ( ) ;
142
+ var buffer = new byte [ 1024 ] ;
143
+ var readCount = 0 ;
144
+ while ( ( readCount = await body . ReadAsync ( buffer ) . DefaultTimeout ( ) ) != - 1 )
145
+ {
146
+ data . AddRange ( buffer . AsMemory ( 0 , readCount ) . ToArray ( ) ) ;
147
+ if ( data . Count == TestData . Length )
148
+ {
149
+ break ;
150
+ }
151
+ }
152
+
153
+ // Sync with client
154
+ await syncPoint . WaitToContinue ( ) ;
155
+
156
+ // Wait for task cancellation
157
+ await cancelledTcs . Task ;
158
+
159
+ readAsyncTask . SetResult ( body . ReadAsync ( buffer ) . AsTask ( ) ) ;
160
+ } , protocol : protocol ) ;
161
+
162
+ var httpClientHandler = new HttpClientHandler ( ) ;
163
+ httpClientHandler . ServerCertificateCustomValidationCallback = HttpClientHandler . DangerousAcceptAnyServerCertificateValidator ;
164
+
165
+ using ( var host = builder . Build ( ) )
166
+ using ( var client = new HttpClient ( httpClientHandler ) )
167
+ {
168
+ await host . StartAsync ( ) . DefaultTimeout ( ) ;
169
+
170
+ var cts = new CancellationTokenSource ( ) ;
171
+ var requestContent = new StreamingHttpContext ( ) ;
172
+
173
+ var request = new HttpRequestMessage ( HttpMethod . Post , $ "https://127.0.0.1:{ host . GetPort ( ) } /") ;
174
+ request . Content = requestContent ;
175
+ request . Version = GetProtocol ( protocol ) ;
176
+ request . VersionPolicy = HttpVersionPolicy . RequestVersionExact ;
177
+
178
+ // Act
179
+ var responseTask = client . SendAsync ( request , cts . Token ) ;
180
+
181
+ var requestStream = await requestContent . GetStreamAsync ( ) . DefaultTimeout ( ) ;
182
+
183
+ // Send headers
184
+ await requestStream . FlushAsync ( ) . DefaultTimeout ( ) ;
185
+ // Write content
186
+ await requestStream . WriteAsync ( TestData ) . DefaultTimeout ( ) ;
187
+
188
+ // Wait until content is read on server
189
+ await syncPoint . WaitForSyncPoint ( ) . DefaultTimeout ( ) ;
190
+
191
+ // Cancel request
192
+ cts . Cancel ( ) ;
193
+
194
+ // Continue on server
195
+ syncPoint . Continue ( ) ;
196
+
197
+ // Assert
198
+ await Assert . ThrowsAnyAsync < OperationCanceledException > ( ( ) => responseTask ) . DefaultTimeout ( ) ;
199
+
200
+ await cancelledTcs . Task . DefaultTimeout ( ) ;
201
+
202
+ var serverWriteTask = await readAsyncTask . Task . DefaultTimeout ( ) ;
203
+
204
+ await Assert . ThrowsAnyAsync < Exception > ( ( ) => serverWriteTask ) . DefaultTimeout ( ) ;
205
+
206
+ await host . StopAsync ( ) . DefaultTimeout ( ) ;
207
+ }
208
+ }
209
+
210
+ // Verify HTTP/2 and HTTP/3 match behavior
211
+ [ ConditionalTheory ]
212
+ [ MsQuicSupported ]
213
+ [ InlineData ( HttpProtocols . Http3 ) ]
214
+ [ InlineData ( HttpProtocols . Http2 ) ]
215
+ public async Task GET_ServerAbort_ClientReceivesAbort ( HttpProtocols protocol )
216
+ {
217
+ // Arrange
218
+ var syncPoint = new SyncPoint ( ) ;
219
+ var cancelledTcs = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
220
+ var writeAsyncTask = new TaskCompletionSource < Task > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
221
+
222
+ var builder = CreateHostBuilder ( async context =>
223
+ {
224
+ context . RequestAborted . Register ( ( ) => cancelledTcs . SetResult ( ) ) ;
225
+
226
+ context . Abort ( ) ;
227
+
228
+ // Sync with client
229
+ await syncPoint . WaitToContinue ( ) ;
230
+
231
+ writeAsyncTask . SetResult ( context . Response . Body . WriteAsync ( TestData ) . AsTask ( ) ) ;
232
+ } , protocol : protocol ) ;
233
+
234
+ var httpClientHandler = new HttpClientHandler ( ) ;
235
+ httpClientHandler . ServerCertificateCustomValidationCallback = HttpClientHandler . DangerousAcceptAnyServerCertificateValidator ;
236
+
237
+ using ( var host = builder . Build ( ) )
238
+ using ( var client = new HttpClient ( httpClientHandler ) )
239
+ {
240
+ await host . StartAsync ( ) . DefaultTimeout ( ) ;
241
+
242
+ var requestContent = new StreamingHttpContext ( ) ;
243
+
244
+ var request = new HttpRequestMessage ( HttpMethod . Get , $ "https://127.0.0.1:{ host . GetPort ( ) } /") ;
245
+ request . Version = GetProtocol ( protocol ) ;
246
+ request . VersionPolicy = HttpVersionPolicy . RequestVersionExact ;
247
+
248
+ // Act
249
+ var ex = await Assert . ThrowsAnyAsync < HttpRequestException > ( ( ) => client . SendAsync ( request ) ) . DefaultTimeout ( ) ;
250
+
251
+ // Assert
252
+ if ( protocol == HttpProtocols . Http3 )
253
+ {
254
+ var innerEx = Assert . IsType < QuicStreamAbortedException > ( ex . InnerException ) ;
255
+ Assert . Equal ( 258 , innerEx . ErrorCode ) ;
256
+ }
257
+
258
+ await cancelledTcs . Task . DefaultTimeout ( ) ;
259
+
260
+ // Sync with server to ensure RequestDelegate is still running
261
+ await syncPoint . WaitForSyncPoint ( ) . DefaultTimeout ( ) ;
262
+ syncPoint . Continue ( ) ;
263
+
264
+ var serverWriteTask = await writeAsyncTask . Task . DefaultTimeout ( ) ;
265
+ await serverWriteTask . DefaultTimeout ( ) ;
266
+
267
+ await host . StopAsync ( ) . DefaultTimeout ( ) ;
268
+ }
269
+ }
270
+
271
+ private static Version GetProtocol ( HttpProtocols protocol )
272
+ {
273
+ switch ( protocol )
274
+ {
275
+ case HttpProtocols . Http2 :
276
+ return HttpVersion . Version20 ;
277
+ case HttpProtocols . Http3 :
278
+ return HttpVersion . Version30 ;
279
+ default :
280
+ throw new InvalidOperationException ( ) ;
281
+ }
282
+ }
283
+
111
284
[ ConditionalFact ]
112
285
[ MsQuicSupported ]
113
286
public async Task GET_MultipleRequestsInSequence_ReusedState ( )
@@ -116,7 +289,7 @@ public async Task GET_MultipleRequestsInSequence_ReusedState()
116
289
object persistedState = null ;
117
290
var requestCount = 0 ;
118
291
119
- var builder = CreateHttp3HostBuilder ( context =>
292
+ var builder = CreateHostBuilder ( context =>
120
293
{
121
294
requestCount ++ ;
122
295
var persistentStateCollection = context . Features . Get < IPersistentStateFeature > ( ) . State ;
@@ -165,17 +338,79 @@ public async Task GET_MultipleRequestsInSequence_ReusedState()
165
338
}
166
339
}
167
340
341
+ [ ConditionalFact ]
342
+ [ MsQuicSupported ]
343
+ public async Task GET_MultipleRequestsInSequence_ReusedRequestHeaderStrings ( )
344
+ {
345
+ // Arrange
346
+ string request1HeaderValue = null ;
347
+ string request2HeaderValue = null ;
348
+ var requestCount = 0 ;
349
+
350
+ var builder = CreateHostBuilder ( context =>
351
+ {
352
+ requestCount ++ ;
353
+
354
+ if ( requestCount == 1 )
355
+ {
356
+ request1HeaderValue = context . Request . Headers . UserAgent ;
357
+ }
358
+ else if ( requestCount == 2 )
359
+ {
360
+ request2HeaderValue = context . Request . Headers . UserAgent ;
361
+ }
362
+ else
363
+ {
364
+ throw new InvalidOperationException ( ) ;
365
+ }
366
+
367
+ return Task . CompletedTask ;
368
+ } ) ;
369
+
370
+ using ( var host = builder . Build ( ) )
371
+ using ( var client = CreateClient ( ) )
372
+ {
373
+ await host . StartAsync ( ) ;
374
+
375
+ // Act
376
+ var request1 = new HttpRequestMessage ( HttpMethod . Get , $ "https://127.0.0.1:{ host . GetPort ( ) } /") ;
377
+ request1 . Headers . TryAddWithoutValidation ( "User-Agent" , "TestUserAgent" ) ;
378
+ request1 . Version = HttpVersion . Version30 ;
379
+ request1 . VersionPolicy = HttpVersionPolicy . RequestVersionExact ;
380
+
381
+ var response1 = await client . SendAsync ( request1 ) ;
382
+ response1 . EnsureSuccessStatusCode ( ) ;
383
+
384
+ // Delay to ensure the stream has enough time to return to pool
385
+ await Task . Delay ( 100 ) ;
386
+
387
+ var request2 = new HttpRequestMessage ( HttpMethod . Get , $ "https://127.0.0.1:{ host . GetPort ( ) } /") ;
388
+ request2 . Headers . TryAddWithoutValidation ( "User-Agent" , "TestUserAgent" ) ;
389
+ request2 . Version = HttpVersion . Version30 ;
390
+ request2 . VersionPolicy = HttpVersionPolicy . RequestVersionExact ;
391
+
392
+ var response2 = await client . SendAsync ( request2 ) ;
393
+ response2 . EnsureSuccessStatusCode ( ) ;
394
+
395
+ // Assert
396
+ Assert . Equal ( "TestUserAgent" , request1HeaderValue ) ;
397
+ Assert . Same ( request1HeaderValue , request2HeaderValue ) ;
398
+
399
+ await host . StopAsync ( ) ;
400
+ }
401
+ }
402
+
168
403
[ ConditionalFact ]
169
404
[ MsQuicSupported ]
170
405
public async Task GET_ConnectionLoggingConfigured_OutputToLogs ( )
171
406
{
172
407
// Arrange
173
- var builder = CreateHttp3HostBuilder (
408
+ var builder = CreateHostBuilder (
174
409
context =>
175
410
{
176
411
return Task . CompletedTask ;
177
412
} ,
178
- kestrel =>
413
+ configureKestrel : kestrel =>
179
414
{
180
415
kestrel . ListenLocalhost ( 5001 , listenOptions =>
181
416
{
@@ -223,7 +458,7 @@ private static HttpClient CreateClient()
223
458
return new HttpClient ( httpHandler ) ;
224
459
}
225
460
226
- private IHostBuilder CreateHttp3HostBuilder ( RequestDelegate requestDelegate , Action < KestrelServerOptions > configureKestrel = null )
461
+ private IHostBuilder CreateHostBuilder ( RequestDelegate requestDelegate , HttpProtocols ? protocol = null , Action < KestrelServerOptions > configureKestrel = null )
227
462
{
228
463
return GetHostBuilder ( )
229
464
. ConfigureWebHost ( webHostBuilder =>
@@ -235,7 +470,7 @@ private IHostBuilder CreateHttp3HostBuilder(RequestDelegate requestDelegate, Act
235
470
{
236
471
o . Listen ( IPAddress . Parse ( "127.0.0.1" ) , 0 , listenOptions =>
237
472
{
238
- listenOptions . Protocols = HttpProtocols . Http3 ;
473
+ listenOptions . Protocols = protocol ?? HttpProtocols . Http3 ;
239
474
listenOptions . UseHttps ( ) ;
240
475
} ) ;
241
476
}
0 commit comments