11// Copyright (c) Microsoft. All rights reserved.
22
33using System . Net ;
4+ using System . Net . Sockets ;
45using Microsoft . Extensions . Logging ;
56
67namespace KernelMemory . Core . Http ;
@@ -19,13 +20,15 @@ public static async Task<HttpResponseMessage> SendAsync(
1920 Func < HttpRequestMessage > requestFactory ,
2021 ILogger logger ,
2122 CancellationToken ct ,
22- Func < TimeSpan , CancellationToken , Task > ? delayAsync = null )
23+ Func < TimeSpan , CancellationToken , Task > ? delayAsync = null ,
24+ TimeSpan ? perAttemptTimeout = null )
2325 {
2426 ArgumentNullException . ThrowIfNull ( httpClient , nameof ( httpClient ) ) ;
2527 ArgumentNullException . ThrowIfNull ( requestFactory , nameof ( requestFactory ) ) ;
2628 ArgumentNullException . ThrowIfNull ( logger , nameof ( logger ) ) ;
2729
2830 delayAsync ??= Task . Delay ;
31+ perAttemptTimeout ??= TimeSpan . FromSeconds ( Constants . HttpRetryDefaults . DefaultPerAttemptTimeoutSeconds ) ;
2932
3033 Exception ? lastException = null ;
3134
@@ -37,7 +40,10 @@ public static async Task<HttpResponseMessage> SendAsync(
3740
3841 try
3942 {
40- var response = await httpClient . SendAsync ( request , ct ) . ConfigureAwait ( false ) ;
43+ using var attemptCts = CancellationTokenSource . CreateLinkedTokenSource ( ct ) ;
44+ attemptCts . CancelAfter ( perAttemptTimeout . Value ) ;
45+
46+ var response = await httpClient . SendAsync ( request , attemptCts . Token ) . ConfigureAwait ( false ) ;
4147
4248 if ( response . IsSuccessStatusCode )
4349 {
@@ -81,7 +87,7 @@ public static async Task<HttpResponseMessage> SendAsync(
8187 catch ( HttpRequestException ex )
8288 {
8389 lastException = ex ;
84- if ( attempt == Constants . HttpRetryDefaults . MaxAttempts )
90+ if ( ! IsRetryableException ( ex ) || attempt == Constants . HttpRetryDefaults . MaxAttempts )
8591 {
8692 throw ;
8793 }
@@ -101,6 +107,20 @@ public static async Task<HttpResponseMessage> SendAsync(
101107 throw lastException ?? new HttpRequestException ( "HTTP call failed after retries" ) ;
102108 }
103109
110+ private static bool IsRetryableException ( HttpRequestException ex )
111+ {
112+ // Fail fast on common "service not running" / "unreachable" conditions.
113+ // Retrying these (especially with default HttpClient timeouts) can make test runs appear hung.
114+ if ( ex . InnerException is SocketException socketEx )
115+ {
116+ return socketEx . SocketErrorCode != SocketError . ConnectionRefused &&
117+ socketEx . SocketErrorCode != SocketError . HostNotFound &&
118+ socketEx . SocketErrorCode != SocketError . NetworkUnreachable ;
119+ }
120+
121+ return true ;
122+ }
123+
104124 private static bool IsRetryableStatusCode ( HttpStatusCode statusCode )
105125 {
106126 return statusCode == HttpStatusCode . TooManyRequests ||
0 commit comments