diff --git a/backend/SchotzIadKmTracker.Api/Program.cs b/backend/SchotzIadKmTracker.Api/Program.cs index a4afe6f..de0a229 100644 --- a/backend/SchotzIadKmTracker.Api/Program.cs +++ b/backend/SchotzIadKmTracker.Api/Program.cs @@ -34,6 +34,21 @@ builder.Services.Configure( // --- Client typé BAN (Base Adresses Nationale) --- // BaseAddress et Timeout proviennent de BANOptions (appsettings.json). +// Fabrique le handler HTTP des clients sortants (BAN, ORS). Le recyclage des +// connexions du pool évite les blocages intermittents dus à une connexion TLS +// gardée ouverte mais fermée côté serveur/NAT, que .NET tentait de réutiliser : +// - PooledConnectionLifetime : durée de vie max d'une connexion avant recyclage ; +// - PooledConnectionIdleTimeout : fermeture des connexions inactives (avant qu'elles +// ne deviennent « éventées ») ; +// - ConnectTimeout : échec rapide si l'ouverture de connexion traîne (le fallback +// Haversine prend alors le relais proprement au lieu de pendre). +static SocketsHttpHandler CreerHandlerHttpSortant() => new() +{ + PooledConnectionLifetime = TimeSpan.FromMinutes(2), + PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30), + ConnectTimeout = TimeSpan.FromSeconds(5) +}; + var banOptions = builder.Configuration .GetSection(BANOptions.SectionName) .Get() ?? new BANOptions(); @@ -42,7 +57,11 @@ builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(banOptions.BaseUrl); client.Timeout = TimeSpan.FromSeconds(banOptions.TimeoutSeconds); -}); +}) +// Recyclage des connexions du pool pour éviter de réutiliser une connexion TLS +// « éventée » (fermée côté serveur/NAT), qui provoquait des blocages intermittents +// jusqu'au timeout. Voir aussi le client ORS ci-dessous. +.ConfigurePrimaryHttpMessageHandler(() => CreerHandlerHttpSortant()); // --- Client typé ORS (OpenRouteService) --- // BaseAddress, Timeout et en-tête de clé d'API proviennent de ORSOptions (appsettings.json). @@ -71,7 +90,10 @@ builder.Services.AddHttpClient(client => // telle quelle et éviter une FormatException à la création du client. client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", orsOptions.ApiKey); } -}); +}) +// Même recyclage des connexions que pour BAN (évite les blocages intermittents sur +// une connexion HTTPS réutilisée alors qu'elle a été fermée côté serveur). +.ConfigurePrimaryHttpMessageHandler(() => CreerHandlerHttpSortant()); // --- Calculateur de distance (orchestration BAN + ORS + fallback Haversine) --- builder.Services.AddScoped(); diff --git a/backend/SchotzIadKmTracker.Api/Services/ORSClient.cs b/backend/SchotzIadKmTracker.Api/Services/ORSClient.cs index cd408df..6e87416 100644 --- a/backend/SchotzIadKmTracker.Api/Services/ORSClient.cs +++ b/backend/SchotzIadKmTracker.Api/Services/ORSClient.cs @@ -50,15 +50,19 @@ public class ORSClient : IORSClient { response = await _httpClient.PostAsJsonAsync(DirectionsGeoJsonPath, body, cancellationToken); } - catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { - // Le HttpClient annule via son Timeout : OperationCanceledException est levée - // sans que le jeton de l'appelant soit annulé -> il s'agit d'un dépassement de délai. - throw new ORSTimeoutException( - "L'appel à OpenRouteService a dépassé le délai imparti.", ex); + // Annulation DEMANDÉE PAR L'APPELANT (ex. requête HTTP entrante annulée par le + // client) : on propage l'annulation telle quelle. Ce n'est PAS un timeout ORS, + // donc on ne la maquille pas en ORSTimeoutException et on ne déclenche pas le + // fallback Haversine à tort. + throw; } - catch (TaskCanceledException ex) + catch (OperationCanceledException ex) { + // Jeton de l'appelant NON déclenché : l'annulation vient du Timeout propre au + // HttpClient ORS -> dépassement de délai réel d'OpenRouteService. (TaskCanceledException + // hérite d'OperationCanceledException : ce bloc couvre les deux.) throw new ORSTimeoutException( "L'appel à OpenRouteService a dépassé le délai imparti.", ex); }