From 7695deae10b72eb12065e0450586ea3de4aabbd6 Mon Sep 17 00:00:00 2001 From: Nikolaz Date: Tue, 13 Oct 2026 20:51:39 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20connexions=20HTTP=20sortantes=20robustes?= =?UTF-8?q?=20(BAN/ORS)=20=E2=80=94=20blocages=20intermittents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les HttpClient vers l'API BAN et ORS réutilisaient des connexions TLS éventées par le pool .NET, provoquant des blocages jusqu'au timeout front de 10s (SocketException Operation canceled). Configuration d'un SocketsHttpHandler dédié avec PooledConnectionLifetime (2 min), PooledConnectionIdleTimeout (30s) et ConnectTimeout (5s). Resserre aussi le catch de ORSClient : une annulation provenant de l'appelant (CancellationToken du contrôleur) est désormais propagée telle quelle, au lieu d'être maquillée en timeout ORS. --- backend/SchotzIadKmTracker.Api/Program.cs | 26 +++++++++++++++++-- .../Services/ORSClient.cs | 16 +++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) 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); }