Per Qodo review on PR #132: SendWithRetryAsync still allowed each
attempt to consume the full per-attempt cap (60s default) even when
the surrounding totalTimeout budget (30s default) was smaller, and
backoff delays could push execution past the budget too. Default
callers that pass no CancellationToken would silently block for
up to ~60s on a hung server before failing.
Cap each attempt at min(perAttemptTimeout, remainingBudget), short-
circuit before starting an attempt or sleeping when the budget is
exhausted, and cap the backoff sleep itself. Preserve the underlying
TaskCanceledException / HttpRequestException as InnerException on
the surfaced HttpRequestException for diagnostics. Caller
cancellation still propagates as OperationCanceledException.
Also bump retry-test timing budgets (per-attempt 50ms→150ms, total
400ms→1.2s) and add a regression test that pins the total-timeout
guarantee under a per-attempt > total config.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR Summary by Qodo
Fix HttpClient retry timeouts to avoid import crash and add regression tests
🐞 Bug fix🧪 Tests🕐 20-40 MinutesWalkthroughs
User Description
Summary
kcap importcrashed mid-run with an unhandledTaskCanceledExceptionwhenever a single HTTP call exceeded the .NET defaultHttpClient.Timeoutof 100s.SendWithRetryAsynconly caughtHttpRequestException, so the timeout-induced cancellation escaped every retry / catch block. Most visibly this killed the classification probe (GET /api/sessions/{id}/last-line) — one slow probe out of 145 would terminate the whole import with no progress shown.CancellationTokenSourcewith an explicit per-attempt cap (60s default), and an exhausted-budget timeout is wrapped asHttpRequestExceptionso the existingcatch (HttpRequestException)blocks at the import / hook call sites degrade the session toProbeError/ "server unreachable" instead of crashing.HttpClientExtensionsRetryTestscover: per-attempt timeout →HttpRequestExceptionafter budget exhausted, retry-and-recover on a single slow attempt, caller cancellation still propagates asOperationCanceledException, transientHttpRequestExceptionretries within budget.Test plan
dotnet run --project test/Capacitor.Cli.Tests.Unit/Capacitor.Cli.Tests.Unit.csproj→ 1283/1283 passdotnet publish src/Capacitor.Cli/Capacitor.Cli.csproj -c Release→ no IL3050/IL2026 AOT warningskcap importagainst a healthy server (no behaviour change)🤖 Generated with Claude Code
AI Description
Diagram
graph TD callers["Import/Hook callers"] --> retry["HttpClientExtensions.SendWithRetryAsync"] --> client["HttpClient"] --> api{{"Capacitor API"}} callerCt(("Caller CT")) --> retry attemptCt(("Per-attempt CTS")) --> retry retry --> outcome["HttpResponse or HttpRequestException"] subgraph Legend direction LR _cmp["Component"] ~~~ _tok(("Token/CTS")) ~~~ _ext{{"External"}} endHigh-Level Assessment
The following are alternative approaches to this PR:
1. Set HttpClient.Timeout explicitly (or to Infinite) at client construction
2. Adopt a resiliency library (e.g., Polly) for retries/timeouts
Recommendation: Keep the PR’s approach: enforcing a linked per-attempt timeout inside SendWithRetryAsync and converting exhausted-budget timeouts to HttpRequestException directly matches existing error-handling expectations at call sites while preserving caller-initiated cancellation semantics. Alternatives were considered, but they either don’t address the call-site exception shape consistently or add unnecessary dependency/complexity for the scope.
File Changes
Bug fix (1)
Tests (1)