-
Notifications
You must be signed in to change notification settings - Fork 4.8k
/
Copy pathHttp2Stream.cs
1674 lines (1427 loc) · 81.6 KB
/
Http2Stream.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http.Headers;
using System.Net.Http.HPack;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;
namespace System.Net.Http
{
internal sealed partial class Http2Connection
{
private sealed class Http2Stream : IValueTaskSource, IHttpStreamHeadersHandler, IHttpTrace
{
private const int InitialStreamBufferSize =
#if DEBUG
10;
#else
1024;
#endif
private static ReadOnlySpan<byte> StatusHeaderName => ":status"u8;
private readonly Http2Connection _connection;
private readonly HttpRequestMessage _request;
private HttpResponseMessage? _response;
/// <summary>Stores any trailers received after returning the response content to the caller.</summary>
private HttpResponseHeaders? _trailers;
private MultiArrayBuffer _responseBuffer; // mutable struct, do not make this readonly
private Http2StreamWindowManager _windowManager;
private CreditWaiter? _creditWaiter;
private int _availableCredit;
private readonly object _creditSyncObject = new object(); // split from SyncObject to avoid lock ordering problems with Http2Connection.SyncObject
private StreamCompletionState _requestCompletionState;
private StreamCompletionState _responseCompletionState;
private ResponseProtocolState _responseProtocolState;
private bool _responseHeadersReceived;
// If this is not null, then we have received a reset from the server
// (i.e. RST_STREAM or general IO error processing the connection)
private Exception? _resetException;
private bool _canRetry; // if _resetException != null, this indicates the stream was refused and so the request is retryable
// This flag indicates that, per section 8.1 of the RFC, the server completed the response and then sent a RST_STREAM with error = NO_ERROR.
// This is a signal to stop sending the request body, but the request is still considered successful.
private bool _requestBodyAbandoned;
/// <summary>
/// The core logic for the IValueTaskSource implementation.
///
/// Thread-safety:
/// _waitSource is used to coordinate between a producer indicating that something is available to process (either the connection's event loop
/// or a cancellation request) and a consumer doing that processing. There must only ever be a single consumer, namely this stream reading
/// data associated with the response. Because there is only ever at most one consumer, producers can trust that if _hasWaiter is true,
/// until the _waitSource is then set, no consumer will attempt to reset the _waitSource. A producer must still take SyncObj in order to
/// coordinate with other producers (e.g. a race between data arriving from the event loop and cancellation being requested), but while holding
/// the lock it can check whether _hasWaiter is true, and if it is, set _hasWaiter to false, exit the lock, and then set the _waitSource. Another
/// producer coming along will then see _hasWaiter as false and will not attempt to concurrently set _waitSource (which would violate _waitSource's
/// thread-safety), and no other consumer could come along in the interim, because _hasWaiter being true means that a consumer is already waiting
/// for _waitSource to be set, and legally there can only be one consumer. Once this producer sets _waitSource, the consumer could quickly loop
/// around to wait again, but invariants have all been maintained in the interim, and the consumer would need to take the SyncObj lock in order to
/// Reset _waitSource.
/// </summary>
private ManualResetValueTaskSourceCore<bool> _waitSource = new ManualResetValueTaskSourceCore<bool> { RunContinuationsAsynchronously = true }; // mutable struct, do not make this readonly
/// <summary>Cancellation registration used to cancel the <see cref="_waitSource"/>.</summary>
private CancellationTokenRegistration _waitSourceCancellation;
/// <summary>
/// Whether code has requested or is about to request a wait be performed and thus requires a call to SetResult to complete it.
/// This is read and written while holding the lock so that most operations on _waitSource don't need to be.
/// </summary>
private bool _hasWaiter;
private readonly CancellationTokenSource? _requestBodyCancellationSource;
private readonly TaskCompletionSource<bool>? _expect100ContinueWaiter;
private int _headerBudgetRemaining;
private bool _sendRstOnResponseClose;
public Http2Stream(HttpRequestMessage request, Http2Connection connection)
{
_request = request;
_connection = connection;
_requestCompletionState = StreamCompletionState.InProgress;
_responseCompletionState = StreamCompletionState.InProgress;
_responseProtocolState = ResponseProtocolState.ExpectingStatus;
_responseBuffer = new MultiArrayBuffer(InitialStreamBufferSize);
_windowManager = new Http2StreamWindowManager(connection, this);
_headerBudgetRemaining = connection._pool.Settings.MaxResponseHeadersByteLength;
// Extended connect requests will use the response content stream for bidirectional communication.
// We will ignore any content set for such requests in SendRequestBodyAsync, as it has no defined semantics.
if (_request.Content == null || _request.IsExtendedConnectRequest)
{
_requestCompletionState = StreamCompletionState.Completed;
if (_request.IsExtendedConnectRequest)
{
_requestBodyCancellationSource = new CancellationTokenSource();
}
}
else
{
// Create this here because it can be canceled before SendRequestBodyAsync is even called.
// To avoid race conditions that can result in this being disposed in response to a server reset
// and then used to issue cancellation, we simply avoid disposing it; that's fine as long as we don't
// construct this via CreateLinkedTokenSource, in which case disposal is necessary to avoid a potential
// leak. If how this is constructed ever changes, we need to revisit disposing it, such as by
// using synchronization (e.g. using an Interlocked.Exchange to "consume" the _requestBodyCancellationSource
// for either disposal or issuing cancellation).
_requestBodyCancellationSource = new CancellationTokenSource();
if (_request.HasHeaders && _request.Headers.ExpectContinue == true)
{
// Create a TCS for handling Expect: 100-continue semantics. See WaitFor100ContinueAsync.
// Note we need to create this in the constructor, because we can receive a 100 Continue response at any time after the constructor finishes.
_expect100ContinueWaiter = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
}
}
_response = new HttpResponseMessage()
{
Version = HttpVersion.Version20,
RequestMessage = _request,
Content = new HttpConnectionResponseContent()
};
}
private object SyncObject => this; // this isn't handed out to code that may lock on it
public void Initialize(int streamId, int initialWindowSize)
{
StreamId = streamId;
_availableCredit = initialWindowSize;
if (NetEventSource.Log.IsEnabled()) Trace($"{nameof(initialWindowSize)}={initialWindowSize}");
}
public int StreamId { get; private set; }
public bool SendRequestFinished => _requestCompletionState != StreamCompletionState.InProgress;
public bool ExpectResponseData => _responseProtocolState == ResponseProtocolState.ExpectingData;
public Http2Connection Connection => _connection;
public bool ConnectProtocolEstablished { get; private set; }
public HttpResponseMessage GetAndClearResponse()
{
// Once SendAsync completes, the Http2Stream should no longer hold onto the response message.
// Since the Http2Stream is rooted by the Http2Connection dictionary, doing so would prevent
// the response stream from being collected and finalized if it were to be dropped without
// being disposed first.
Debug.Assert(_response != null);
HttpResponseMessage r = _response;
_response = null;
return r;
}
public async Task SendRequestBodyAsync(CancellationToken cancellationToken)
{
// Extended connect requests will use the response content stream for bidirectional communication.
// Ignore any content set for such requests, as it has no defined semantics.
if (_request.Content == null || _request.IsExtendedConnectRequest)
{
Debug.Assert(_requestCompletionState == StreamCompletionState.Completed);
return;
}
if (NetEventSource.Log.IsEnabled()) Trace($"{_request.Content}");
Debug.Assert(_requestBodyCancellationSource != null);
// Cancel the request body sending if cancellation is requested on the supplied cancellation token.
// Normally we might create a linked token, but once cancellation is requested, we can't recover anyway,
// so it's fine to cancel the source representing the whole request body, and doing so allows us to avoid
// creating another CTS instance and the associated nodes inside of it. With this, cancellation will be
// requested on _requestBodyCancellationSource when we need to cancel the request stream for any reason,
// such as receiving an RST_STREAM or when the passed in token has cancellation requested. However, to
// avoid unnecessarily registering with the cancellation token unless we have to, we wait to do so until
// either we know we need to do a Expect: 100-continue send or until we know that the copying of our
// content completed asynchronously.
CancellationTokenRegistration linkedRegistration = default;
bool sendRequestContent = true;
try
{
if (_expect100ContinueWaiter != null)
{
linkedRegistration = RegisterRequestBodyCancellation(cancellationToken);
sendRequestContent = await WaitFor100ContinueAsync(_requestBodyCancellationSource.Token).ConfigureAwait(false);
}
if (sendRequestContent)
{
using var writeStream = new Http2WriteStream(this, _request.Content.Headers.ContentLength.GetValueOrDefault(-1));
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestContentStart();
ValueTask vt = _request.Content.InternalCopyToAsync(writeStream, context: null, _requestBodyCancellationSource.Token);
if (vt.IsCompleted)
{
vt.GetAwaiter().GetResult();
}
else
{
if (linkedRegistration.Equals(default))
{
linkedRegistration = RegisterRequestBodyCancellation(cancellationToken);
}
await vt.ConfigureAwait(false);
}
if (writeStream.BytesWritten < writeStream.ContentLength)
{
// The number of bytes we actually sent doesn't match the advertised Content-Length
throw new HttpRequestException(SR.Format(SR.net_http_request_content_length_mismatch, writeStream.BytesWritten, writeStream.ContentLength));
}
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestContentStop(writeStream.BytesWritten);
}
if (NetEventSource.Log.IsEnabled()) Trace($"Finished sending request body.");
}
catch (Exception e)
{
if (NetEventSource.Log.IsEnabled()) Trace($"Failed to send request body: {e}");
bool signalWaiter;
Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
Debug.Assert(_requestCompletionState == StreamCompletionState.InProgress, $"Request already completed with state={_requestCompletionState}");
if (_requestBodyAbandoned)
{
// See comments on _requestBodyAbandoned.
// In this case, the request is still considered successful and we do not want to send a RST_STREAM,
// and we also don't want to propagate any error to the caller, in particular for non-duplex scenarios.
Debug.Assert(_responseCompletionState == StreamCompletionState.Completed);
_requestCompletionState = StreamCompletionState.Completed;
Debug.Assert(!ConnectProtocolEstablished);
Complete();
return;
}
// This should not cause RST_STREAM to be sent because the request is still marked as in progress.
bool sendReset;
(signalWaiter, sendReset) = CancelResponseBody();
Debug.Assert(!sendReset);
_requestCompletionState = StreamCompletionState.Failed;
SendReset();
Debug.Assert(!ConnectProtocolEstablished);
Complete();
}
if (signalWaiter)
{
_waitSource.SetResult(true);
}
throw;
}
finally
{
linkedRegistration.Dispose();
}
// New scope here to avoid variable name conflict on "sendReset"
{
Debug.Assert(!Monitor.IsEntered(SyncObject));
bool sendReset = false;
lock (SyncObject)
{
Debug.Assert(_requestCompletionState == StreamCompletionState.InProgress, $"Request already completed with state={_requestCompletionState}");
_requestCompletionState = StreamCompletionState.Completed;
bool complete = false;
if (_responseCompletionState != StreamCompletionState.InProgress)
{
// Note, we can reach this point if the response stream failed but cancellation didn't propagate before we finished.
sendReset = _responseCompletionState == StreamCompletionState.Failed;
complete = true;
}
if (sendReset)
{
SendReset();
}
else if (!sendRequestContent)
{
// Request body hasn't been sent, so we need to notify the server that it won't
// get the body. However, we cannot do it right here because the server can
// reset the whole stream before we will have a chance to read the response body.
_sendRstOnResponseClose = true;
}
else
{
// Send EndStream asynchronously and without cancellation.
// If this fails, it means that the connection is aborting and we will be reset.
_connection.LogExceptions(_connection.SendEndStreamAsync(StreamId));
}
if (complete)
{
Debug.Assert(!ConnectProtocolEstablished);
Complete();
}
}
}
}
// Delay sending request body if we sent Expect: 100-continue.
// We can either get 100 response from server and send body
// or we may exceed timeout and send request body anyway.
// If we get response status >= 300, we will not send the request body.
public async ValueTask<bool> WaitFor100ContinueAsync(CancellationToken cancellationToken)
{
Debug.Assert(_request?.Content != null);
if (NetEventSource.Log.IsEnabled()) Trace($"Waiting to send request body content for 100-Continue.");
// Use TCS created in constructor. It will complete when one of three things occurs:
// 1. we receive the relevant response from the server.
// 2. the timer fires before we receive the relevant response from the server.
// 3. cancellation is requested before we receive the relevant response from the server.
// We need to run the continuation asynchronously for cases 1 and 3 (for 1 so that we don't starve the body copy operation, and
// for 3 so that we don't run a lot of work as part of code calling Cancel), so the TCS is created to run continuations asynchronously.
// We await the created Timer's disposal so that we ensure any work associated with it has quiesced prior to this method
// returning, just in case this object is pooled and potentially reused for another operation in the future.
TaskCompletionSource<bool> waiter = _expect100ContinueWaiter!;
using (cancellationToken.UnsafeRegister(static s => ((TaskCompletionSource<bool>)s!).TrySetResult(false), waiter))
await using (new Timer(static s =>
{
var thisRef = (Http2Stream)s!;
if (NetEventSource.Log.IsEnabled()) thisRef.Trace($"100-Continue timer expired.");
thisRef._expect100ContinueWaiter?.TrySetResult(true);
}, this, _connection._pool.Settings._expect100ContinueTimeout, Timeout.InfiniteTimeSpan).ConfigureAwait(false))
{
bool shouldSendContent = await waiter.Task.ConfigureAwait(false);
// By now, either we got a response from the server or the timer expired or cancellation was requested.
CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
return shouldSendContent;
}
}
private void SendReset()
{
Debug.Assert(Monitor.IsEntered(SyncObject));
Debug.Assert(_requestCompletionState != StreamCompletionState.InProgress);
Debug.Assert(_responseCompletionState != StreamCompletionState.InProgress);
Debug.Assert(_requestCompletionState == StreamCompletionState.Failed || _responseCompletionState == StreamCompletionState.Failed,
"Reset called but neither request nor response is failed");
if (NetEventSource.Log.IsEnabled()) Trace($"Stream reset. Request={_requestCompletionState}, Response={_responseCompletionState}.");
// Don't send a RST_STREAM if we've already received one from the server.
if (_resetException == null)
{
// If execution reached this line, it's guaranteed that
// _requestCompletionState == StreamCompletionState.Failed or _responseCompletionState == StreamCompletionState.Failed
_connection.LogExceptions(_connection.SendRstStreamAsync(StreamId, Http2ProtocolErrorCode.Cancel));
}
}
private void Complete()
{
Debug.Assert(Monitor.IsEntered(SyncObject));
Debug.Assert(_requestCompletionState != StreamCompletionState.InProgress);
Debug.Assert(_responseCompletionState != StreamCompletionState.InProgress);
if (NetEventSource.Log.IsEnabled()) Trace($"Stream complete. Request={_requestCompletionState}, Response={_responseCompletionState}.");
_connection.RemoveStream(this);
lock (_creditSyncObject)
{
CreditWaiter? waiter = _creditWaiter;
if (waiter != null)
{
waiter.Dispose();
_creditWaiter = null;
}
}
}
private void Cancel()
{
if (NetEventSource.Log.IsEnabled()) Trace("");
CancellationTokenSource? requestBodyCancellationSource = null;
bool signalWaiter = false;
bool sendReset = false;
Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
if (_requestCompletionState == StreamCompletionState.InProgress)
{
requestBodyCancellationSource = _requestBodyCancellationSource;
Debug.Assert(requestBodyCancellationSource != null);
}
(signalWaiter, sendReset) = CancelResponseBody();
}
// When cancellation propagates, SendRequestBodyAsync will set _requestCompletionState to Failed
requestBodyCancellationSource?.Cancel();
lock (SyncObject)
{
if (sendReset)
{
SendReset();
// Extended CONNECT notes:
//
// To prevent from calling it *twice*, Extended CONNECT stream's Complete() is only
// called from CloseResponseBody(), as CloseResponseBody() is *always* called
// from Extended CONNECT stream's Dispose().
if (!ConnectProtocolEstablished)
{
Complete();
}
}
}
if (signalWaiter)
{
_waitSource.SetResult(true);
}
}
// Returns whether the waiter should be signalled or not.
private (bool signalWaiter, bool sendReset) CancelResponseBody()
{
Debug.Assert(Monitor.IsEntered(SyncObject));
bool sendReset = _sendRstOnResponseClose;
if (_responseCompletionState == StreamCompletionState.InProgress)
{
_responseCompletionState = StreamCompletionState.Failed;
if (_requestCompletionState != StreamCompletionState.InProgress)
{
sendReset = true;
}
}
// Discard any remaining buffered response data
_responseBuffer.DiscardAll();
_responseProtocolState = ResponseProtocolState.Aborted;
bool signalWaiter = _hasWaiter;
_hasWaiter = false;
return (signalWaiter, sendReset);
}
public void OnWindowUpdate(int amount)
{
lock (_creditSyncObject)
{
_availableCredit = checked(_availableCredit + amount);
if (_availableCredit > 0 && _creditWaiter != null)
{
int granted = Math.Min(_availableCredit, _creditWaiter.Amount);
if (_creditWaiter.TrySetResult(granted))
{
_availableCredit -= granted;
}
}
}
}
private const int FirstHPackRequestPseudoHeaderId = 1;
private const int LastHPackRequestPseudoHeaderId = 7;
private const int FirstHPackStatusPseudoHeaderId = 8;
private const int LastHPackStatusPseudoHeaderId = 14;
private const int FirstHPackNormalHeaderId = 15;
private const int LastHPackNormalHeaderId = 61;
private static ReadOnlySpan<int> HpackStaticStatusCodeTable => [200, 204, 206, 304, 400, 404, 500];
private static readonly (HeaderDescriptor descriptor, byte[] value)[] s_hpackStaticHeaderTable = new (HeaderDescriptor, byte[])[LastHPackNormalHeaderId - FirstHPackNormalHeaderId + 1]
{
(KnownHeaders.AcceptCharset.Descriptor, Array.Empty<byte>()),
(KnownHeaders.AcceptEncoding.Descriptor, "gzip, deflate"u8.ToArray()),
(KnownHeaders.AcceptLanguage.Descriptor, Array.Empty<byte>()),
(KnownHeaders.AcceptRanges.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Accept.Descriptor, Array.Empty<byte>()),
(KnownHeaders.AccessControlAllowOrigin.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Age.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Allow.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Authorization.Descriptor, Array.Empty<byte>()),
(KnownHeaders.CacheControl.Descriptor, Array.Empty<byte>()),
(KnownHeaders.ContentDisposition.Descriptor, Array.Empty<byte>()),
(KnownHeaders.ContentEncoding.Descriptor, Array.Empty<byte>()),
(KnownHeaders.ContentLanguage.Descriptor, Array.Empty<byte>()),
(KnownHeaders.ContentLength.Descriptor, Array.Empty<byte>()),
(KnownHeaders.ContentLocation.Descriptor, Array.Empty<byte>()),
(KnownHeaders.ContentRange.Descriptor, Array.Empty<byte>()),
(KnownHeaders.ContentType.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Cookie.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Date.Descriptor, Array.Empty<byte>()),
(KnownHeaders.ETag.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Expect.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Expires.Descriptor, Array.Empty<byte>()),
(KnownHeaders.From.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Host.Descriptor, Array.Empty<byte>()),
(KnownHeaders.IfMatch.Descriptor, Array.Empty<byte>()),
(KnownHeaders.IfModifiedSince.Descriptor, Array.Empty<byte>()),
(KnownHeaders.IfNoneMatch.Descriptor, Array.Empty<byte>()),
(KnownHeaders.IfRange.Descriptor, Array.Empty<byte>()),
(KnownHeaders.IfUnmodifiedSince.Descriptor, Array.Empty<byte>()),
(KnownHeaders.LastModified.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Link.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Location.Descriptor, Array.Empty<byte>()),
(KnownHeaders.MaxForwards.Descriptor, Array.Empty<byte>()),
(KnownHeaders.ProxyAuthenticate.Descriptor, Array.Empty<byte>()),
(KnownHeaders.ProxyAuthorization.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Range.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Referer.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Refresh.Descriptor, Array.Empty<byte>()),
(KnownHeaders.RetryAfter.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Server.Descriptor, Array.Empty<byte>()),
(KnownHeaders.SetCookie.Descriptor, Array.Empty<byte>()),
(KnownHeaders.StrictTransportSecurity.Descriptor, Array.Empty<byte>()),
(KnownHeaders.TransferEncoding.Descriptor, Array.Empty<byte>()),
(KnownHeaders.UserAgent.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Vary.Descriptor, Array.Empty<byte>()),
(KnownHeaders.Via.Descriptor, Array.Empty<byte>()),
(KnownHeaders.WWWAuthenticate.Descriptor, Array.Empty<byte>()),
};
void IHttpStreamHeadersHandler.OnStaticIndexedHeader(int index)
{
Debug.Assert(index >= FirstHPackRequestPseudoHeaderId && index <= LastHPackNormalHeaderId);
if (index <= LastHPackRequestPseudoHeaderId)
{
if (NetEventSource.Log.IsEnabled()) Trace($"Invalid request pseudo-header ID {index}.");
throw new HttpRequestException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response);
}
else if (index <= LastHPackStatusPseudoHeaderId)
{
int statusCode = HpackStaticStatusCodeTable[index - FirstHPackStatusPseudoHeaderId];
OnStatus(statusCode);
}
else
{
(HeaderDescriptor descriptor, byte[] value) = s_hpackStaticHeaderTable[index - FirstHPackNormalHeaderId];
OnHeader(descriptor, value);
}
}
void IHttpStreamHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
Debug.Assert(index >= FirstHPackRequestPseudoHeaderId && index <= LastHPackNormalHeaderId);
if (index <= LastHPackRequestPseudoHeaderId)
{
if (NetEventSource.Log.IsEnabled()) Trace($"Invalid request pseudo-header ID {index}.");
throw new HttpRequestException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response);
}
else if (index <= LastHPackStatusPseudoHeaderId)
{
int statusCode = ParseStatusCode(value);
OnStatus(statusCode);
}
else
{
(HeaderDescriptor descriptor, _) = s_hpackStaticHeaderTable[index - FirstHPackNormalHeaderId];
OnHeader(descriptor, value);
}
}
void IHttpStreamHeadersHandler.OnDynamicIndexedHeader(int? index, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
OnHeader(name, value);
}
private void AdjustHeaderBudget(int amount)
{
_headerBudgetRemaining -= amount;
if (_headerBudgetRemaining < 0)
{
throw new HttpRequestException(HttpRequestError.ConfigurationLimitExceeded, SR.Format(SR.net_http_response_headers_exceeded_length, _connection._pool.Settings.MaxResponseHeadersByteLength));
}
}
private void OnStatus(int statusCode)
{
if (NetEventSource.Log.IsEnabled()) Trace($"Status code is {statusCode}");
AdjustHeaderBudget(10); // for ":status" plus 3-digit status code
Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
if (_responseProtocolState == ResponseProtocolState.Aborted)
{
// We could have aborted while processing the header block.
return;
}
if (_responseProtocolState == ResponseProtocolState.ExpectingHeaders)
{
if (NetEventSource.Log.IsEnabled()) Trace("Received extra status header.");
throw new HttpRequestException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response_multiple_status_codes);
}
if (_responseProtocolState != ResponseProtocolState.ExpectingStatus)
{
// Pseudo-headers are allowed only in header block
if (NetEventSource.Log.IsEnabled()) Trace($"Status pseudo-header received in {_responseProtocolState} state.");
throw new HttpRequestException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response_pseudo_header_in_trailer);
}
Debug.Assert(_response != null);
_response.StatusCode = (HttpStatusCode)statusCode;
if (statusCode < 200)
{
// We do not process headers from 1xx responses.
_responseProtocolState = ResponseProtocolState.ExpectingIgnoredHeaders;
if (_response.StatusCode == HttpStatusCode.Continue && _expect100ContinueWaiter != null)
{
if (NetEventSource.Log.IsEnabled()) Trace("Received 100-Continue status.");
_expect100ContinueWaiter.TrySetResult(true);
}
}
else
{
if (statusCode == 200 && _response.RequestMessage!.IsExtendedConnectRequest)
{
ConnectProtocolEstablished = true;
}
_responseProtocolState = ResponseProtocolState.ExpectingHeaders;
// If we are waiting for a 100-continue response, signal the waiter now.
if (_expect100ContinueWaiter != null)
{
// If the final status code is >= 300, skip sending the body.
bool shouldSendBody = (statusCode < 300);
if (NetEventSource.Log.IsEnabled()) Trace($"Expecting 100 Continue but received final status {statusCode}.");
_expect100ContinueWaiter.TrySetResult(shouldSendBody);
}
}
}
}
private void OnHeader(HeaderDescriptor descriptor, ReadOnlySpan<byte> value)
{
if (NetEventSource.Log.IsEnabled()) Trace($"{descriptor.Name}: {Encoding.ASCII.GetString(value)}");
AdjustHeaderBudget(descriptor.Name.Length + value.Length);
Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
if (_responseProtocolState == ResponseProtocolState.Aborted)
{
// We could have aborted while processing the header block.
return;
}
if (_responseProtocolState == ResponseProtocolState.ExpectingIgnoredHeaders)
{
// for 1xx response we ignore all headers.
return;
}
if (_responseProtocolState != ResponseProtocolState.ExpectingHeaders && _responseProtocolState != ResponseProtocolState.ExpectingTrailingHeaders)
{
if (NetEventSource.Log.IsEnabled()) Trace("Received header before status.");
throw new HttpRequestException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response);
}
Encoding? valueEncoding = _connection._pool.Settings._responseHeaderEncodingSelector?.Invoke(descriptor.Name, _request);
// Note we ignore the return value from TryAddWithoutValidation;
// if the header can't be added, we silently drop it.
if (_responseProtocolState == ResponseProtocolState.ExpectingTrailingHeaders)
{
Debug.Assert(_trailers != null);
string headerValue = descriptor.GetHeaderValue(value, valueEncoding);
_trailers.TryAddWithoutValidation((descriptor.HeaderType & HttpHeaderType.Request) == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue);
}
else if ((descriptor.HeaderType & HttpHeaderType.Content) == HttpHeaderType.Content)
{
Debug.Assert(_response != null && _response.Content != null);
string headerValue = descriptor.GetHeaderValue(value, valueEncoding);
_response.Content.Headers.TryAddWithoutValidation(descriptor, headerValue);
}
else
{
Debug.Assert(_response != null);
string headerValue = _connection.GetResponseHeaderValueWithCaching(descriptor, value, valueEncoding);
_response.Headers.TryAddWithoutValidation((descriptor.HeaderType & HttpHeaderType.Request) == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue);
}
}
}
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
Debug.Assert(name.Length > 0);
if (name[0] == (byte)':')
{
// Pseudo-header
if (name.SequenceEqual(StatusHeaderName))
{
int statusCode = ParseStatusCode(value);
OnStatus(statusCode);
}
else
{
if (NetEventSource.Log.IsEnabled()) Trace($"Invalid response pseudo-header '{Encoding.ASCII.GetString(name)}'.");
throw new HttpRequestException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response);
}
}
else
{
// Regular header
if (!HeaderDescriptor.TryGet(name, out HeaderDescriptor descriptor))
{
// Invalid header name
throw new HttpRequestException(HttpRequestError.InvalidResponse, SR.Format(SR.net_http_invalid_response_header_name, Encoding.ASCII.GetString(name)));
}
OnHeader(descriptor, value);
}
}
public void OnHeadersStart()
{
Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
switch (_responseProtocolState)
{
case ResponseProtocolState.ExpectingStatus:
case ResponseProtocolState.Aborted:
break;
case ResponseProtocolState.ExpectingData:
_responseProtocolState = ResponseProtocolState.ExpectingTrailingHeaders;
_trailers ??= new HttpResponseHeaders(containsTrailingHeaders: true);
break;
default:
ThrowProtocolError();
break;
}
}
}
public void OnHeadersComplete(bool endStream)
{
Debug.Assert(!Monitor.IsEntered(SyncObject));
bool signalWaiter;
lock (SyncObject)
{
switch (_responseProtocolState)
{
case ResponseProtocolState.Aborted:
return;
case ResponseProtocolState.ExpectingHeaders:
_responseProtocolState = endStream ? ResponseProtocolState.Complete : ResponseProtocolState.ExpectingData;
_responseHeadersReceived = true;
break;
case ResponseProtocolState.ExpectingTrailingHeaders:
if (!endStream)
{
if (NetEventSource.Log.IsEnabled()) Trace("Trailing headers received without endStream");
ThrowProtocolError();
}
_responseProtocolState = ResponseProtocolState.Complete;
break;
case ResponseProtocolState.ExpectingIgnoredHeaders:
if (endStream)
{
// we should not get endStream while processing 1xx response.
ThrowProtocolError();
}
// We should wait for final response before signaling to waiter.
_responseProtocolState = ResponseProtocolState.ExpectingStatus;
return;
default:
ThrowProtocolError();
break;
}
if (endStream)
{
Debug.Assert(_responseCompletionState == StreamCompletionState.InProgress, $"Response already completed with state={_responseCompletionState}");
_responseCompletionState = StreamCompletionState.Completed;
// Extended CONNECT notes:
//
// To prevent from calling it *prematurely*, Extended CONNECT stream's Complete() is only
// called from CloseResponseBody(), as CloseResponseBody() is *only* called
// from Extended CONNECT stream's Dispose().
//
// Due to bidirectional streaming nature of the Extended CONNECT request,
// the *write side* of the stream can only be completed by calling Dispose().
//
// The streaming in both ways happens over the single "response" stream instance, which makes
// _requestCompletionState *not indicative* of the actual state of the write side of the stream.
if (_requestCompletionState == StreamCompletionState.Completed && !ConnectProtocolEstablished)
{
Complete();
}
// We should never reach here with the request failed. It's only set to Failed in SendRequestBodyAsync after we've called Cancel,
// which will set the _responseCompletionState to Failed, meaning we'll never get here.
Debug.Assert(_requestCompletionState != StreamCompletionState.Failed);
}
if (_responseProtocolState == ResponseProtocolState.ExpectingData)
{
_windowManager.Start();
}
signalWaiter = _hasWaiter;
_hasWaiter = false;
}
if (signalWaiter)
{
_waitSource.SetResult(true);
}
}
public void OnResponseData(ReadOnlySpan<byte> buffer, bool endStream)
{
Debug.Assert(!Monitor.IsEntered(SyncObject));
bool signalWaiter;
lock (SyncObject)
{
switch (_responseProtocolState)
{
case ResponseProtocolState.ExpectingData:
break;
case ResponseProtocolState.Aborted:
return;
default:
// Flow control messages are not valid in this state.
ThrowProtocolError();
break;
}
if (_responseBuffer.ActiveMemory.Length + buffer.Length > _windowManager.StreamWindowSize)
{
// Window size exceeded.
ThrowProtocolError(Http2ProtocolErrorCode.FlowControlError);
}
_responseBuffer.EnsureAvailableSpace(buffer.Length);
_responseBuffer.AvailableMemory.CopyFrom(buffer);
_responseBuffer.Commit(buffer.Length);
if (endStream)
{
_responseProtocolState = ResponseProtocolState.Complete;
Debug.Assert(_responseCompletionState == StreamCompletionState.InProgress, $"Response already completed with state={_responseCompletionState}");
_responseCompletionState = StreamCompletionState.Completed;
// Extended CONNECT notes:
//
// To prevent from calling it *prematurely*, Extended CONNECT stream's Complete() is only
// called from CloseResponseBody(), as CloseResponseBody() is *only* called
// from Extended CONNECT stream's Dispose().
//
// Due to bidirectional streaming nature of the Extended CONNECT request,
// the *write side* of the stream can only be completed by calling Dispose().
//
// The streaming in both ways happens over the single "response" stream instance, which makes
// _requestCompletionState *not indicative* of the actual state of the write side of the stream.
if (_requestCompletionState == StreamCompletionState.Completed && !ConnectProtocolEstablished)
{
Complete();
}
// We should never reach here with the request failed. It's only set to Failed in SendRequestBodyAsync after we've called Cancel,
// which will set the _responseCompletionState to Failed, meaning we'll never get here.
Debug.Assert(_requestCompletionState != StreamCompletionState.Failed);
}
signalWaiter = _hasWaiter;
_hasWaiter = false;
}
if (signalWaiter)
{
_waitSource.SetResult(true);
}
}
// This is called in several different cases:
// (1) Receiving RST_STREAM on this stream. If so, the resetStreamErrorCode will be non-null, and canRetry will be true only if the error code was REFUSED_STREAM.
// (2) Receiving GOAWAY that indicates this stream has not been processed. If so, canRetry will be true.
// (3) Connection IO failure or protocol violation. If so, resetException will contain the relevant exception and canRetry will be false.
// (4) Receiving EOF from the server. If so, resetException will contain an exception like "expected 9 bytes of data", and canRetry will be false.
public void OnReset(Exception resetException, Http2ProtocolErrorCode? resetStreamErrorCode = null, bool canRetry = false)
{
if (NetEventSource.Log.IsEnabled()) Trace($"{nameof(resetException)}={resetException}, {nameof(resetStreamErrorCode)}={resetStreamErrorCode}");
bool cancel = false;
CancellationTokenSource? requestBodyCancellationSource = null;
Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
// If we've already finished, don't actually reset the stream.
// Otherwise, any waiters that haven't executed yet will see the _resetException and throw.
// This can happen, for example, when the server finishes the request and then closes the connection,
// but the waiter hasn't woken up yet.
if (_requestCompletionState == StreamCompletionState.Completed && _responseCompletionState == StreamCompletionState.Completed)
{
return;
}
// It's possible we could be called twice, e.g. we receive a RST_STREAM and then the whole connection dies
// before we have a chance to process cancellation and tear everything down. Just ignore this.
if (_resetException != null)
{
return;
}
// If the server told us the request has not been processed (via Last-Stream-ID on GOAWAY),
// but we've already received some response data from the server, then the server lied to us.
// In this case, don't allow the request to be retried.
if (canRetry && _responseProtocolState != ResponseProtocolState.ExpectingStatus)
{
canRetry = false;
}
// Per section 8.1 in the RFC:
// If the server has completed the response body (i.e. we've received EndStream)
// but the request body is still sending, and we then receive a RST_STREAM with errorCode = NO_ERROR,
// we treat this specially and simply cancel sending the request body, rather than treating
// the entire request as failed.
if (resetStreamErrorCode == Http2ProtocolErrorCode.NoError &&
_responseCompletionState == StreamCompletionState.Completed)
{
if (_requestCompletionState == StreamCompletionState.InProgress)
{
_requestBodyAbandoned = true;
requestBodyCancellationSource = _requestBodyCancellationSource;
Debug.Assert(requestBodyCancellationSource != null);
}
}
else
{
_resetException = resetException;
_canRetry = canRetry;
cancel = true;
}
}
if (requestBodyCancellationSource != null)
{