From 2e9beef860b1b3c3cc22cea0ff11fa4762d84f21 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Fri, 23 Oct 2026 08:37:04 -0400 Subject: [PATCH] Give CLI package queries two minutes and name the timeout when it fires The CLI gave every GET under /packages/ (search, details, versions, installed, updates) 30 seconds. On Windows CI runners an npm search or details call routinely takes 6-15 s and sometimes passes 30 s, so cli-headless-e2e failed intermittently on `package search/details --manager npm` and passed on re-run. Users on a slow network hit the same limit. When the limit fired, the cancellation surfaced as the bare "The operation was canceled.", which reads like a cancelled package operation. A client-side timeout now raises a TimeoutException naming the request and the limit, and the status probe still treats it as an unreachable endpoint. Co-Authored-By: Claude Opus 5.5 --- src/UniGetUI.Interface.IpcApi/IpcClient.cs | 32 +++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/UniGetUI.Interface.IpcApi/IpcClient.cs b/src/UniGetUI.Interface.IpcApi/IpcClient.cs index 1931b8e6f5..4299316703 100644 --- a/src/UniGetUI.Interface.IpcApi/IpcClient.cs +++ b/src/UniGetUI.Interface.IpcApi/IpcClient.cs @@ -1181,20 +1181,31 @@ private async Task SendAsync( HttpContent? requestContent = null ) { - using var timeout = new CancellationTokenSource(GetRequestTimeout(method, relativePath)); + TimeSpan requestTimeout = GetRequestTimeout(method, relativePath); + using var timeout = new CancellationTokenSource(requestTimeout); using var request = new HttpRequestMessage(method, BuildRelativeUri(relativePath, queryParameters)); request.Content = requestContent; - using var response = await _httpClient.SendAsync(request, timeout.Token); - string content = await response.Content.ReadAsStringAsync(); + try + { + using var response = await _httpClient.SendAsync(request, timeout.Token); + string content = await response.Content.ReadAsStringAsync(timeout.Token); - if (!response.IsSuccessStatusCode) + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException( + string.IsNullOrWhiteSpace(content) ? response.ReasonPhrase : content + ); + } + + return content; + } + catch (OperationCanceledException ex) when (timeout.IsCancellationRequested) { - throw new InvalidOperationException( - string.IsNullOrWhiteSpace(content) ? response.ReasonPhrase : content + throw new TimeoutException( + $"UniGetUI did not answer {method.Method} {relativePath} within {requestTimeout.TotalSeconds:0} seconds.", + ex ); } - - return content; } private async Task ReadAuthenticatedJsonAsync( @@ -1474,7 +1485,7 @@ private static TimeSpan GetRequestTimeout(HttpMethod method, string relativePath { return method == HttpMethod.Post ? TimeSpan.FromMinutes(5) - : TimeSpan.FromSeconds(30); + : TimeSpan.FromMinutes(2); } if (IpcHttpRoutes.StartsWith(relativePath, "/bundles/install")) @@ -1536,7 +1547,8 @@ private static bool IsConnectivityException(Exception exception) return exception is HttpRequestException or IOException or TaskCanceledException - or OperationCanceledException; + or OperationCanceledException + or TimeoutException; } private static IpcEndpointRegistration? SelectLiveRegistration(