fix: surface HttpClient timeouts as PveApiException(RequestTimeout)

When HttpClient.Timeout elapses, .NET throws TaskCanceledException — not
HttpRequestException — so the existing catch in PveHttpClient.SendAsync
missed it and callers got a raw stack trace. With -TimeoutSeconds now
configurable and documented, this gap became user-visible.

In .NET 5+ HttpClient surfaces transport timeouts as TaskCanceledException
with a TimeoutException inner; user-driven token cancellation does not.
Catch by that inner-type signature and rethrow as PveApiException with
HttpStatusCode.RequestTimeout, the resource path, and a message that
reports the configured timeout.

Adds SendAsync_TimeoutFires_ThrowsPveApiExceptionWithRequestTimeout which
swaps in a delaying HttpMessageHandler with a 50ms timeout to exercise
the path deterministically. Drops the redundant
DefaultSessionTimeoutIs100Seconds test (covered by
PveSessionTests.Timeout_DefaultIs100Seconds and the existing flow-through
test).

Addresses PR #61 review feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-05-20 18:17:31 -05:00
parent ea2bcdc336
commit a1d9550d83
2 changed files with 47 additions and 3 deletions
@@ -353,6 +353,17 @@ namespace PSProxmoxVE.Core.Client
{
response = await _httpClient.SendAsync(request).ConfigureAwait(false);
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
// HttpClient.Timeout elapsed. In .NET 5+, HttpClient surfaces transport
// timeouts as TaskCanceledException with a TimeoutException inner —
// distinguishing them from caller-driven CancellationToken cancellation.
var seconds = _httpClient.Timeout == System.Threading.Timeout.InfiniteTimeSpan
? "infinite"
: _httpClient.Timeout.TotalSeconds.ToString("0", System.Globalization.CultureInfo.InvariantCulture) + "s";
throw new PveApiException(HttpStatusCode.RequestTimeout,
$"Request timed out after {seconds}.", resource, httpMethod, ex);
}
catch (HttpRequestException ex)
{
throw new PveApiException(HttpStatusCode.ServiceUnavailable,