fix(llc): reconnect on a server-side processing timeout, and stop parsing errors twice - #172
Conversation
…sing errors twice `ERROR_LAYER.md` says a 408 is about the moment rather than a verdict on the request, so it retries with backoff — but `isAutomaticReconnectionEnabled` fell through to the 4xx rule and refused it. The code now honours the contract the doc states. `ServerInitiated`'s inner switch ended in `_ => true`, reachable only for a null error since all four exception kinds are matched above. Written as `null => true`, a fifth kind becomes a compile error rather than a silent reconnect. `AuthInterceptor.onError` classified every error to find the one code it acts on, then handed the raw failure to `ApiErrorInterceptor`, which read the same body again. It now forwards the `StreamDioException` it already has — the same one `ApiErrorInterceptor` would have built. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## feat/error-layer #172 +/- ##
====================================================
+ Coverage 66.18% 66.21% +0.03%
====================================================
Files 205 205
Lines 8306 8315 +9
====================================================
+ Hits 5497 5506 +9
Misses 2809 2809 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Three of the five review findings on #168. The two left out are deliberate: see the end.
1. A 408 refused to reconnect, contradicting the documented contract
ERROR_LAYER.mdsays 408 is "a server-side processing timeout, not a verdict on the request" and retries with backoff.isAutomaticReconnectionEnabledfell through to the 4xx rule and refused it:Verified by running it, not just reading:
Fixed in the code rather than by documenting the divergence, so the doc keeps stating one rule and the code stops disagreeing.
Risk: none against this backend. A 408 cannot reach the client over a socket. Every WS error frame comes from
Conn.CloseWithError(monolith/server/ws/server.go:225,monolith/engine/websocket/conn.go:429) — the only two producers ofNewConnectionErrorEvent— and its three call sites inmonolith/server/base.goproduce aReadAuthMessagefailure (400-class,:1660), an auth error (401/403-class,:1699) or a rate limit (429,:1760/:1783). All five producers ofRequestTimeoutErrorare HTTP request-body or upload paths. So the new branch is inert today; it exists so a reader reconciling doc against code doesn't have to discover that to make sense of it.Pinned by a test that fails when the branch is removed.
2.
_ => truehid a non-exhaustive switchServerInitiated's inner switch matched all fourStreamExceptionkinds and then ended in_ => true, reachable only for anullerror. Written asnull => true, adding a fifth kind is a compile error instead of a silent default-to-reconnect. No behaviour change.3. Every error response was parsed twice
AuthInterceptor.onErrorcalledtoStreamException()— which runsStreamApiError.fromJsonon the body — to find the one code it acts on, discarded it for everything else, then handed the raw failure toApiErrorInterceptor, documented as installed last, which read the same body again.Gating on
statusCodewould have been the obvious fix and is wrong: the layer's own rule is thatstatusCodeandcodeare independent and neither may be inferred from the other. Instead it forwards what it already has — and the machinery was already there, sinceApiErrorInterceptorshort-circuits onStreamDioExceptionandtoStreamException()does too. The forwarded exception is the same objectApiErrorInterceptorwould have constructed, so this is behaviour-preserving by construction.Deliberately not in this PR
TokenProviderthat throws its own error type on a transient failure gets wrapped intoStreamAuthenticationExceptionand is then non-reconnectable — on the exact path the SDK reconnects to fix. That changes runtime behaviour on a common path and is the PR author's call. Worth noting for whoever decides:DefaultRetryStrategyhas no attempt cap, so "retriable" means unbounded retries at ≤25s intervals — which is already whatSystemInitiated,UnHealthyConnection,ConnectTimeoutand network-causedAuthenticationFaileddo. Whichever way it goes, the missing test is the app-supplied-error case; the two Stream-typed cases are already pinned.Retry-AfterHTTP-date parsing. Nothing to do. The backend has one emission site,monolith/budget/middleware.go:399,strconv.FormatInt, and the runbook confirms "only on 429". There is no date form to drop.715 tests pass,
dart analyzeanddart formatclean.🤖 Generated with Claude Code