@@ -236,7 +236,8 @@ async ValueTask<int> ReadAheadWithZeroByteReadAsync()
236236
237237 private bool CheckKeepAliveTimeoutExceeded ( )
238238 {
239- // We only honor a Keep-Alive timeout on HTTP/1.0 responses.
239+ // We intentionally honor the Keep-Alive timeout on all HTTP/1.X versions, not just 1.0. This is to maximize compat with
240+ // servers that use a lower idle timeout than the client, but give us a hint in the form of a Keep-Alive timeout parameter.
240241 // If _keepAliveTimeoutSeconds is 0, no timeout has been set.
241242 return _keepAliveTimeoutSeconds != 0 &&
242243 GetIdleTicks ( Environment . TickCount64 ) >= _keepAliveTimeoutSeconds * 1000 ;
@@ -665,11 +666,6 @@ public async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request,
665666 ParseHeaderNameValue ( this , line . Span , response , isFromTrailer : false ) ;
666667 }
667668
668- if ( response . Version . Minor == 0 )
669- {
670- ProcessHttp10KeepAliveHeader ( response ) ;
671- }
672-
673669 if ( HttpTelemetry . Log . IsEnabled ( ) ) HttpTelemetry . Log . ResponseHeadersStop ( ) ;
674670
675671 if ( allowExpect100ToContinue != null )
@@ -1124,49 +1120,58 @@ private static void ParseHeaderNameValue(HttpConnection connection, ReadOnlySpan
11241120 }
11251121 else
11261122 {
1127- // Request headers returned on the response must be treated as custom headers.
11281123 string headerValue = connection . GetResponseHeaderValueWithCaching ( descriptor , value , valueEncoding ) ;
1124+
1125+ if ( descriptor . Equals ( KnownHeaders . KeepAlive ) )
1126+ {
1127+ // We are intentionally going against RFC to honor the Keep-Alive header even if
1128+ // we haven't received a Keep-Alive connection token to maximize compat with servers.
1129+ connection . ProcessKeepAliveHeader ( headerValue ) ;
1130+ }
1131+
1132+ // Request headers returned on the response must be treated as custom headers.
11291133 response . Headers . TryAddWithoutValidation (
11301134 ( descriptor . HeaderType & HttpHeaderType . Request ) == HttpHeaderType . Request ? descriptor . AsCustomHeader ( ) : descriptor ,
11311135 headerValue ) ;
11321136 }
11331137 }
11341138
1135- private void ProcessHttp10KeepAliveHeader ( HttpResponseMessage response )
1139+ private void ProcessKeepAliveHeader ( string keepAlive )
11361140 {
1137- if ( response . Headers . NonValidated . TryGetValues ( KnownHeaders . KeepAlive . Name , out HeaderStringValues keepAliveValues ) )
1138- {
1139- string keepAlive = keepAliveValues . ToString ( ) ;
1140- var parsedValues = new UnvalidatedObjectCollection < NameValueHeaderValue > ( ) ;
1141+ var parsedValues = new UnvalidatedObjectCollection < NameValueHeaderValue > ( ) ;
11411142
1142- if ( NameValueHeaderValue . GetNameValueListLength ( keepAlive , 0 , ',' , parsedValues ) == keepAlive . Length )
1143+ if ( NameValueHeaderValue . GetNameValueListLength ( keepAlive , 0 , ',' , parsedValues ) == keepAlive . Length )
1144+ {
1145+ foreach ( NameValueHeaderValue nameValue in parsedValues )
11431146 {
1144- foreach ( NameValueHeaderValue nameValue in parsedValues )
1147+ // The HTTP/1.1 spec does not define any parameters for the Keep-Alive header, so we are using the de facto standard ones - timeout and max.
1148+ if ( string . Equals ( nameValue . Name , "timeout" , StringComparison . OrdinalIgnoreCase ) )
11451149 {
1146- if ( string . Equals ( nameValue . Name , "timeout" , StringComparison . OrdinalIgnoreCase ) )
1150+ if ( ! string . IsNullOrEmpty ( nameValue . Value ) &&
1151+ HeaderUtilities . TryParseInt32 ( nameValue . Value , out int timeout ) &&
1152+ timeout >= 0 )
11471153 {
1148- if ( ! string . IsNullOrEmpty ( nameValue . Value ) &&
1149- HeaderUtilities . TryParseInt32 ( nameValue . Value , out int timeout ) &&
1150- timeout >= 0 )
1154+ // Some servers are very strict with closing the connection exactly at the timeout.
1155+ // Avoid using the connection if it is about to exceed the timeout to avoid resulting request failures.
1156+ const int OffsetSeconds = 1 ;
1157+
1158+ if ( timeout <= OffsetSeconds )
11511159 {
1152- if ( timeout == 0 )
1153- {
1154- _connectionClose = true ;
1155- }
1156- else
1157- {
1158- _keepAliveTimeoutSeconds = timeout ;
1159- }
1160+ _connectionClose = true ;
11601161 }
1161- }
1162- else if ( string . Equals ( nameValue . Name , "max" , StringComparison . OrdinalIgnoreCase ) )
1163- {
1164- if ( nameValue . Value == "0" )
1162+ else
11651163 {
1166- _connectionClose = true ;
1164+ _keepAliveTimeoutSeconds = timeout - OffsetSeconds ;
11671165 }
11681166 }
11691167 }
1168+ else if ( string . Equals ( nameValue . Name , "max" , StringComparison . OrdinalIgnoreCase ) )
1169+ {
1170+ if ( nameValue . Value == "0" )
1171+ {
1172+ _connectionClose = true ;
1173+ }
1174+ }
11701175 }
11711176 }
11721177 }
0 commit comments