fix: surface TCP connect timeout as TimeoutException#237
Conversation
TcpStreamTransport.ConnectAsync enforces its connection timeout with a CancellationTokenSource + Task.WaitAsync, so an unreachable device surfaced as TaskCanceledException - semantically misleading, since nothing was canceled by the caller. This read like an app bug to consumers and caused daqifi/daqifi-desktop#517 (a Sentry-captured "error" for what is an environmental condition). No caller-supplied token is involved in this path, so the cancellation can only mean a timeout: translate it to TimeoutException with the endpoint and configured timeout in the message. The retry loop's behavior (retries, StatusChanged events) is unchanged - the translated exception simply flows through it. Behavioral API note: consumers catching TaskCanceledException or OperationCanceledException around ConnectAsync must now catch TimeoutException instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code Review by Qodo
1.
|
PR Summary by QodoSurface TCP connect timeouts as TimeoutException WalkthroughsDescription• Translate internal connect timeouts from OperationCanceled/TaskCanceled to TimeoutException. • Include endpoint + timeout details in the thrown exception message. • Add regression test ensuring StatusChanged also carries the translated exception. Diagramgraph TD
A["Caller"] --> B["TcpStreamTransport.ConnectAsync"] --> C["TcpClient.ConnectAsync"] --> D["WaitAsync + timeout"]
C --> E{{"Remote endpoint"}}
D --> F["NetworkStream open"] --> G["StatusChanged event"] --> A
D --> H["TimeoutException"] --> G
High-Level AssessmentThe following are alternative approaches to this PR: 1. Use Task.WaitAsync(TimeSpan) directly
2. Add optional caller CancellationToken to ConnectAsync
Recommendation: Current approach is appropriate: it preserves the existing retry/status behavior while fixing the semantic mismatch by surfacing timeouts as TimeoutException (with helpful endpoint/timeout context). If the codebase targets a framework where Task.WaitAsync(TimeSpan) is available and preferred, consider switching to that to avoid cancellation-token-based timeout signaling, but the current implementation is already correct and well-tested. File ChangesBug fix (1)
Tests (1)
|
… exception - Add an internal ConnectTaskFactory test seam so the timeout-translation test substitutes a never-completing connect task instead of relying on an unroutable address hanging (environment-dependent, could fail fast with SocketException on some networks). - Preserve the caught OperationCanceledException as InnerException on the translated TimeoutException, and assert it in the test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re Qodo finding 2 (timeout drops inner exception): fixed in 2c9b04b — the translated |
Summary
TcpStreamTransport.ConnectAsync(ConnectionRetryOptions?)enforces its connection timeout with aCancellationTokenSource+Task.WaitAsync(cts.Token), so an unreachable device surfaced asTaskCanceledException— semantically misleading, since nothing was canceled by the caller; the connect attempt timed out. This read like an app bug to consumers and caused daqifi/daqifi-desktop#517, where Sentry auto-filed the stack trace as an error for what is an environmental condition (device powered off, stale discovery entry, wrong subnet).No caller-supplied cancellation token is involved in this path, so the cancellation can only ever mean a timeout. This PR catches it at the
WaitAsynccall (when (cts.IsCancellationRequested)) and rethrows asTimeoutExceptionwith the endpoint and configured timeout in the message, e.g.:The retry loop's behavior is unchanged — the translated exception flows through the existing generic catch, so retries,
StatusChangedevents, and the final rethrow all carry theTimeoutException.Behavioral API note
Consumers catching
TaskCanceledException/OperationCanceledExceptionaroundConnectAsyncto detect connect timeouts must now catchTimeoutException— worth a release-notes mention. daqifi-desktop's connect-failure classification (daqifi/daqifi-desktop#595) handles the old exception type and the new one falls into the same handling tier on its side once it picks this up... (it currently special-casesOperationCanceledException;TimeoutExceptionwill need the same warning-level classification when the package is bumped — tracked on the desktop side.)Tests
New
TcpStreamTransport_ConnectAsync_WhenConnectTimesOut_ThrowsTimeoutException: connects to RFC 5737 TEST-NET-1 (192.0.2.1, unroutable — same address the existing local-interface bind test uses) with a 250ms timeout, assertsTimeoutExceptionnaming the endpoint, and asserts theStatusChangedevent carries the translated exception. Full suite green: 1008 passed on net9.0 and net10.0.🤖 Generated with Claude Code