17
17
using Microsoft . AspNetCore . Http ;
18
18
using Microsoft . AspNetCore . Http . Features ;
19
19
using Microsoft . AspNetCore . Server . Kestrel . Core ;
20
+ using Microsoft . AspNetCore . Server . Kestrel . Core . Internal . Http ;
20
21
using Microsoft . AspNetCore . Server . Kestrel . Core . Internal . Infrastructure ;
21
22
using Microsoft . AspNetCore . Server . Kestrel . Https ;
22
23
using Microsoft . AspNetCore . Server . Kestrel . Https . Internal ;
@@ -756,6 +757,9 @@ public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLarg
756
757
} ;
757
758
758
759
testContext . InitializeHeartbeat ( ) ;
760
+ var dateHeaderValueManager = new DateHeaderValueManager ( ) ;
761
+ dateHeaderValueManager . OnHeartbeat ( DateTimeOffset . MinValue ) ;
762
+ testContext . DateHeaderValueManager = dateHeaderValueManager ;
759
763
760
764
var listenOptions = new ListenOptions ( new IPEndPoint ( IPAddress . Loopback , 0 ) ) ;
761
765
@@ -782,16 +786,23 @@ async Task App(HttpContext context)
782
786
await connection . Send (
783
787
"GET / HTTP/1.1" ,
784
788
"Host:" ,
785
- "Connection: close" ,
786
789
"" ,
787
790
"" ) ;
788
791
789
- var minTotalOutputSize = chunkCount * chunkSize ;
792
+ await connection . Receive (
793
+ "HTTP/1.1 200 OK" ,
794
+ $ "Date: { dateHeaderValueManager . GetDateHeaderValues ( ) . String } ") ;
790
795
791
796
// Make sure consuming a single chunk exceeds the 2 second timeout.
792
797
var targetBytesPerSecond = chunkSize / 4 ;
793
- await AssertStreamCompleted ( connection . Stream , minTotalOutputSize , targetBytesPerSecond ) ;
798
+
799
+ // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless
800
+ // the response header writing logic or response body chunking logic itself changes.
801
+ await AssertBytesReceivedAtTargetRate ( connection . Stream , expectedBytes : 33_553_537 , targetBytesPerSecond ) ;
794
802
await appFuncCompleted . Task . DefaultTimeout ( ) ;
803
+
804
+ connection . ShutdownSend ( ) ;
805
+ await connection . WaitForConnectionClose ( ) ;
795
806
}
796
807
await server . StopAsync ( ) ;
797
808
}
@@ -801,11 +812,7 @@ await connection.Send(
801
812
Assert . False ( requestAborted ) ;
802
813
}
803
814
804
- private bool ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeadersRetryPredicate ( Exception e )
805
- => e is IOException && e . Message . Contains ( "Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request" ) ;
806
-
807
815
[ Fact ]
808
- [ Flaky ( "https://github.com/dotnet/corefx/issues/30691" , FlakyOn . AzP . Windows ) ]
809
816
[ CollectDump ]
810
817
public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeaders ( )
811
818
{
@@ -830,6 +837,9 @@ public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLarg
830
837
} ;
831
838
832
839
testContext . InitializeHeartbeat ( ) ;
840
+ var dateHeaderValueManager = new DateHeaderValueManager ( ) ;
841
+ dateHeaderValueManager . OnHeartbeat ( DateTimeOffset . MinValue ) ;
842
+ testContext . DateHeaderValueManager = dateHeaderValueManager ;
833
843
834
844
var listenOptions = new ListenOptions ( new IPEndPoint ( IPAddress . Loopback , 0 ) ) ;
835
845
@@ -859,6 +869,86 @@ await connection.Send(
859
869
"" ) ;
860
870
}
861
871
872
+ await connection . Send (
873
+ "GET / HTTP/1.1" ,
874
+ "Host:" ,
875
+ "" ,
876
+ "" ) ;
877
+
878
+ await connection . Receive (
879
+ "HTTP/1.1 200 OK" ,
880
+ $ "Date: { dateHeaderValueManager . GetDateHeaderValues ( ) . String } ") ;
881
+
882
+ var minResponseSize = headerSize * headerCount ;
883
+ var minTotalOutputSize = requestCount * minResponseSize ;
884
+
885
+ // Make sure consuming a single set of response headers exceeds the 2 second timeout.
886
+ var targetBytesPerSecond = minResponseSize / 4 ;
887
+
888
+ // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless
889
+ // the response header writing logic itself changes.
890
+ await AssertBytesReceivedAtTargetRate ( connection . Stream , expectedBytes : 268_439_596 , targetBytesPerSecond ) ;
891
+ connection . ShutdownSend ( ) ;
892
+ await connection . WaitForConnectionClose ( ) ;
893
+ }
894
+
895
+ await server . StopAsync ( ) ;
896
+ }
897
+
898
+ mockKestrelTrace . Verify ( t => t . ResponseMinimumDataRateNotSatisfied ( It . IsAny < string > ( ) , It . IsAny < string > ( ) ) , Times . Never ( ) ) ;
899
+ mockKestrelTrace . Verify ( t => t . ConnectionStop ( It . IsAny < string > ( ) ) , Times . Once ( ) ) ;
900
+ Assert . False ( requestAborted ) ;
901
+ }
902
+
903
+ [ Fact ]
904
+ [ Flaky ( "https://github.com/aspnet/AspNetCore/issues/13219" , FlakyOn . AzP . Linux , FlakyOn . Helix . All ) ]
905
+ public async Task ClientCanReceiveFullConnectionCloseResponseWithoutErrorAtALowDataRate ( )
906
+ {
907
+ var chunkSize = 64 * 128 * 1024 ;
908
+ var chunkCount = 4 ;
909
+ var chunkData = new byte [ chunkSize ] ;
910
+
911
+ var requestAborted = false ;
912
+ var appFuncCompleted = new TaskCompletionSource < object > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
913
+ var mockKestrelTrace = new Mock < IKestrelTrace > ( ) ;
914
+
915
+ var testContext = new TestServiceContext ( LoggerFactory , mockKestrelTrace . Object )
916
+ {
917
+ ServerOptions =
918
+ {
919
+ Limits =
920
+ {
921
+ MinResponseDataRate = new MinDataRate ( bytesPerSecond : 240 , gracePeriod : TimeSpan . FromSeconds ( 2 ) )
922
+ }
923
+ }
924
+ } ;
925
+
926
+ testContext . InitializeHeartbeat ( ) ;
927
+ var dateHeaderValueManager = new DateHeaderValueManager ( ) ;
928
+ dateHeaderValueManager . OnHeartbeat ( DateTimeOffset . MinValue ) ;
929
+ testContext . DateHeaderValueManager = dateHeaderValueManager ;
930
+
931
+ var listenOptions = new ListenOptions ( new IPEndPoint ( IPAddress . Loopback , 0 ) ) ;
932
+
933
+ async Task App ( HttpContext context )
934
+ {
935
+ context . RequestAborted . Register ( ( ) =>
936
+ {
937
+ requestAborted = true ;
938
+ } ) ;
939
+
940
+ for ( var i = 0 ; i < chunkCount ; i ++ )
941
+ {
942
+ await context . Response . BodyWriter . WriteAsync ( new Memory < byte > ( chunkData , 0 , chunkData . Length ) , context . RequestAborted ) ;
943
+ }
944
+
945
+ appFuncCompleted . SetResult ( null ) ;
946
+ }
947
+
948
+ using ( var server = new TestServer ( App , testContext , listenOptions ) )
949
+ {
950
+ using ( var connection = server . CreateConnection ( ) )
951
+ {
862
952
// Close the connection with the last request so AssertStreamCompleted actually completes.
863
953
await connection . Send (
864
954
"GET / HTTP/1.1" ,
@@ -867,12 +957,18 @@ await connection.Send(
867
957
"" ,
868
958
"" ) ;
869
959
870
- var responseSize = headerSize * headerCount ;
871
- var minTotalOutputSize = requestCount * responseSize ;
960
+ await connection . Receive (
961
+ "HTTP/1.1 200 OK" ,
962
+ "Connection: close" ,
963
+ $ "Date: { dateHeaderValueManager . GetDateHeaderValues ( ) . String } ") ;
964
+
965
+ // Make sure consuming a single chunk exceeds the 2 second timeout.
966
+ var targetBytesPerSecond = chunkSize / 4 ;
872
967
873
- // Make sure consuming a single set of response headers exceeds the 2 second timeout.
874
- var targetBytesPerSecond = responseSize / 4 ;
875
- await AssertStreamCompleted ( connection . Stream , minTotalOutputSize , targetBytesPerSecond ) ;
968
+ // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless
969
+ // the response header writing logic or response body chunking logic itself changes.
970
+ await AssertStreamCompletedAtTargetRate ( connection . Stream , expectedBytes : 33_553_556 , targetBytesPerSecond ) ;
971
+ await appFuncCompleted . Task . DefaultTimeout ( ) ;
876
972
}
877
973
await server . StopAsync ( ) ;
878
974
}
@@ -909,7 +1005,30 @@ private async Task AssertStreamAborted(Stream stream, int totalBytes)
909
1005
Assert . True ( totalReceived < totalBytes , $ "{ nameof ( AssertStreamAborted ) } Stream completed successfully.") ;
910
1006
}
911
1007
912
- private async Task AssertStreamCompleted ( Stream stream , long minimumBytes , int targetBytesPerSecond )
1008
+ private async Task AssertBytesReceivedAtTargetRate ( Stream stream , int expectedBytes , int targetBytesPerSecond )
1009
+ {
1010
+ var receiveBuffer = new byte [ 64 * 1024 ] ;
1011
+ var totalReceived = 0 ;
1012
+ var startTime = DateTimeOffset . UtcNow ;
1013
+
1014
+ do
1015
+ {
1016
+ var received = await stream . ReadAsync ( receiveBuffer , 0 , Math . Min ( receiveBuffer . Length , expectedBytes - totalReceived ) ) ;
1017
+
1018
+ Assert . NotEqual ( 0 , received ) ;
1019
+
1020
+ totalReceived += received ;
1021
+
1022
+ var expectedTimeElapsed = TimeSpan . FromSeconds ( totalReceived / targetBytesPerSecond ) ;
1023
+ var timeElapsed = DateTimeOffset . UtcNow - startTime ;
1024
+ if ( timeElapsed < expectedTimeElapsed )
1025
+ {
1026
+ await Task . Delay ( expectedTimeElapsed - timeElapsed ) ;
1027
+ }
1028
+ } while ( totalReceived < expectedBytes ) ;
1029
+ }
1030
+
1031
+ private async Task AssertStreamCompletedAtTargetRate ( Stream stream , long expectedBytes , int targetBytesPerSecond )
913
1032
{
914
1033
var receiveBuffer = new byte [ 64 * 1024 ] ;
915
1034
var received = 0 ;
@@ -929,7 +1048,7 @@ private async Task AssertStreamCompleted(Stream stream, long minimumBytes, int t
929
1048
}
930
1049
} while ( received > 0 ) ;
931
1050
932
- Assert . True ( totalReceived >= minimumBytes , $ " { nameof ( AssertStreamCompleted ) } Stream aborted prematurely." ) ;
1051
+ Assert . Equal ( expectedBytes , totalReceived ) ;
933
1052
}
934
1053
935
1054
public static TheoryData < string , StringValues , string > NullHeaderData
0 commit comments