Classify RPC errors and recover quietly from Durable Object resets - #58
Classify RPC errors and recover quietly from Durable Object resets#58ndisidore wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Review — Classify RPC errors and recover quietly from DO resets
I read the whole diff against the PR base (c637d9f), verified the capnweb error-serialization semantics the classifier depends on, and ran the frontend suite + backend/shared/frontend builds. Solid, well-scoped work — the commit split, the doc-comments, and the "loud on purpose" annotations make the intent easy to follow.
Verification I ran
vitest run src/rpcErrors.test.ts→ 17 passed, including the capnweb transport-string canary.pnpm --filter workshop-backend --filter workshop-shared buildand--filter workshop-frontend build→ all green (covers theAuthenticatedApiImplstub refactor and the sharedAUTH_ERROR_*additions).
The load-bearing assumption checks out
The whole design rests on custom Error props (durableObjectReset, overloaded, retryable, durableObjectId, and the new code) surviving the WebSocket round-trip. I confirmed this against the installed capnweb 0.8.0:
- Serialize walks
Object.keys(e)and captures every own-enumerable prop except name/message/stack (dist/index.cjs:1515). - Deserialize restores them onto a real
Errorsubclass instance (dist/index.cjs:1697,:1709), so bothflag(err, …)anderr instanceof ErrorinmessageOfbehave as intended.
So "flags/codes are authoritative, messages are fallback" is genuinely true over this transport — good.
The backend stub fix is the right call
Turning the cached user stub into a per-call getter (server.ts get user()) is what actually makes the frontend retries reachable, and the doc-comment explaining "poisoned incarnation → fresh stub per attempt" is exactly the kind of kernel comment that belongs here. Migrating the two raw auth throws to createAuthError(...) and confirming there are no other stringly-typed auth throws left (I grepped — only the message table and tests reference the literals) closes the drift loop cleanly.
Suggestions (non-blocking)
-
The canary guards messages but not the property round-trip. The classifier's primary path is flag-based, yet the only capnweb-pinned behavior is
CONNECTION_MESSAGES. The reject-frame test uses a syntheticObject.assign(new Error(...), {...}), so a hypothetical capnweb regression that stopped serializing custom Error props would pass CI green while silently reverting every flag-based classification to the message fallback (and droppingcodeentirely → auth errors misclassified). Consider one integration-style assertion that an Error with a custom prop survives an actual capnweb devaluate/evaluate round-trip, so the assumption I verified by hand is pinned the same way the strings are. -
get user()now mints a fresh stub on every access, including repeated accesses within one method. e.g.server.ts:215-216readsthis.user.id.toString()thenthis.user.id.name!back-to-back, and#isAdmin()is called alongside otherthis.userreads in the same request. The comment correctly notes stub creation is local/lazy, so this is a micro-cost, not a correctness issue — but a couple of hot methods that touchthis.userseveral times could bind it to aconstlocally to avoid the churn. Purely optional. -
'auth'currently has no consumer beyond makingisTransientRpcErrorreturn false. That's the intended safety property (never quiet/retry auth failures), so this is fine — just flagging that the wholeAUTH_ERROR_*shared surface exists to encode a negative right now. Worth a one-line comment at theclassifyRpcErrorauth branch noting the class is deliberately terminal, so a future reader doesn't assume there's a missing "re-auth" handler.
Nothing here blocks merge. The retries are correctly confined to idempotent reads/subscribes, writes keep the inline-hint-not-auto-retry behavior (the double-send reasoning in the chat-send path is right), and the ObserverConfigModal/Connections "loud on purpose" carve-outs show the quieting was applied thoughtfully rather than blanket.
|
Review posted to PR #58. SummaryI reviewed PR #58 (RPC error classification + quiet DO-reset recovery), verified its core assumptions, and posted a review. What I checked:
Verdict: Solid, well-scoped, mergeable. The backend per-call stub getter is what makes the retries reachable and is correctly the elegant kernel fix. Retries are confined to idempotent reads/subscribes; writes keep the inline-hint (not auto-retry) behavior with correct double-send reasoning. Non-blocking suggestions raised:
|
19d7150 to
6af1085
Compare
There was a problem hiding this comment.
Review — Classify RPC errors and recover quietly from DO resets
I re-read the full diff against the base (5d8cc25), ran the classifier suite myself, and independently checked the load-bearing assumptions. This is solid, well-scoped work and the earlier non-blocking feedback has been folded in cleanly.
Verification I ran
vitest run src/rpcErrors.test.ts→ 20 passed (up from 17 — the new round-trip test is included).- Re-read
rpcErrors.ts, theserver.ts/user.tsstub + auth-code changes, theAUTH_ERROR_*additions inworkshop-shared, and every frontend call site (ChatInterface,routes/index,__root,gatekeepers,SidebarWorkspaces,ObserverConfigModal,Connections, …).
Prior suggestions are addressed
- Property round-trip is now pinned.
rpcErrors.test.ts:208-218runs a realdeserialize(serialize(...))and assertsdurableObjectReset/overloaded/durableObjectId/codeall survive and that the result still classifies asdo-reset. This is exactly the gap I'd have flagged: the flag path is primary, and now a capnweb serialization regression fails CI instead of silently demoting everything to message-matching. Good. - Terminal
authis documented.rpcErrors.ts:64-65states the class is deliberately terminal with no missing re-auth handler — a future reader won't mistake it for an omission. - The
get user()micro-cost was explicitly optional; leaving it is fine since the getter is a local, lazythis.users.get(id), not a network call.
The design holds up
- Classifier precedence is correct.
do-reset(:59) is checked beforeconnection(:61) andauth(:66), so a reset frame that also carriesoverloaded/retryablestill resolves todo-reset— matched by the test at:48. - Retries are correctly confined to idempotent reads/subscribes.
withDoResetRetryrefuses flagless local transport errors (:101), so a retry never fires through a closure-captured dead stub; connection-manager recovery keeps ownership of that path. The chat-send path stays inline-hint-not-auto-retry, and the double-send reasoning (a reset after commit) is right. - The backend stub fix is the reachability fix.
server.ts get user()re-resolving per call is what lets a retried read hit a fresh incarnation; a cached stub would stay poisoned. This is the correct kernel change and the doc-comment earns its place. - Auth drift is closed. Both raw throws migrated to
createAuthError, and the non-Base64 token path inuser.tsnow classifies as an auth failure rather than leaking the decoder'sSyntaxError— a nice catch. - The "loud on purpose" carve-outs in
Connections/ObserverConfigModal(no retry path → don't quiet) show the quieting was applied per-surface, not blanket.
Non-blocking nits
ObserverConfigModal.tsx:131wrapsPromise.all([...])inwithDoResetRetry. If the first call resolves but the second rejects with a reset, the retry re-runs both reads. Both are idempotent gatekeeper-metadata reads so it's harmless, just slightly more work than necessary — not worth changing.withDoResetRetryaroundsubscribeConnectedAccountsis safe (the failing attempt returns no stub, and the.thendisposes oncancelled), but it's worth keeping in mind that any future non-idempotent subscribe with side effects on open would need a different wrapper. The doc-comment's "never use for writes" covers the intent.
Nothing blocks merge. LGTM.
|
Review posted to PR #58. SummaryI independently re-reviewed PR #58 rather than rubber-stamping the prior review, verified its core assumptions myself, and posted a fresh review. What I checked:
Key finding — the prior review's suggestions are now implemented:
Verdict: Solid, well-scoped, mergeable. The backend per-call |
There was a problem hiding this comment.
Review — Classify RPC errors and recover quietly from DO resets
I re-reviewed the full diff against the base (5d8cc25) independently, ran the classifier suite, and typechecked the kernel changes rather than relying on the two prior reviews.
Verification I ran
vitest run src/rpcErrors.test.ts→ 20 passed, including the capnweb transport-string canary and the newserialize/deserializecustom-property round-trip (rpcErrors.test.ts:208-218).types:checkonworkshop-backend+workshop-shared→ green (covers theget user()refactor and the sharedAUTH_ERROR_*surface).
The design holds up
- Flags-first classifier, precedence is correct.
do-reset(rpcErrors.ts:59) is checked beforeconnection(:61) andauth(:66), so a reset frame that also carriesoverloaded/retryablestill resolves todo-reset(pinned by the test at:48). The round-trip test now closes the one gap I'd have flagged: a capnweb regression that dropped custom Error props would fail CI instead of silently demoting everything to message-matching. - Backend stub fix is the reachability fix. Turning the cached stub into a per-call
get user()(server.ts:93) is what lets a retried read hit a fresh incarnation; the doc-comment ("poisoned incarnation → fresh stub per attempt") earns its place in the kernel. I checked the subscription path:subscribeConnectedAccounts(server.ts:332) returns a stub bound to the resolved incarnation, so a reset that fails the first attempt leaves nothing registered on a live incarnation and the retry can't leak a dangling subscription. - Retries confined to idempotent reads/subscribes.
withDoResetRetryrefuses flagless local transport errors (rpcErrors.ts:101), so a retry never fires through a closure-captured dead stub — the connection manager keeps ownership of that recovery. Writes keep inline-hint-not-auto-retry; the double-send reasoning (a reset after commit) is right. - Auth drift closed. Both raw throws migrated to
createAuthError, and the non-Base64 token path inuser.ts:302now classifies as an auth failure instead of leaking the decoder'sSyntaxError— a good catch.
One observation on the auth message fallback (non-blocking)
AUTH_MESSAGES is matched with message.includes(m) (rpcErrors.ts:66), and one value — "invalid session token" — is a short, generic phrase. A future unrelated error whose message merely contains that substring would misclassify as auth. That's harmless here because auth is the terminal class (never quieted, never retried), so the misclassification can only ever make the UX more conservative, never wrongly quiet/retry a real failure — and AUTH_ERROR_MESSAGES is documented as a fallback for older deployments where the code didn't survive. Worth keeping in mind if the fallback list ever grows with genuinely ambiguous phrases; not worth changing now.
The "loud on purpose" carve-outs are right
Connections.tsx:73 and ObserverConfigModal.tsx:120 deliberately keep console.error + toast because those surfaces have no retry path — a quieted transient there would strand the user on a permanent loader / empty panel. Applying the quieting per-surface rather than blanket is the correct call.
Nothing blocks merge. Solid, well-scoped, and the commit split keeps the kernel changes easy to read apart from the UI. LGTM.
|
Review posted to PR #58. SummaryI independently re-reviewed PR #58 by reading the full diff against the base ( What I verified myself:
One new observation the prior reviews didn't surface: the auth message fallback matches Verdict: Solid, well-scoped, mergeable. LGTM. |
6af1085 to
3e00b45
Compare
There was a problem hiding this comment.
Review — Classify RPC errors and recover quietly from DO resets
I reviewed the full diff against the base (010dd53) independently — reading the classifier, the backend get user()/auth-code changes, the shared AUTH_ERROR_* surface, and every frontend call site — and re-ran the checks myself rather than relying on the prior reviews.
Verification I ran (from a clean install)
vitest run src/rpcErrors.test.ts→ 20/20 pass, including both canaries (the transport-string pin and theserialize/deserializecustom-property round-trip atrpcErrors.test.ts:208).types:checkonworkshop-backend+workshop-shared+workshop-frontend→ all green.lint:check→ exit 0 (no new warnings from any file this PR touches).
The design holds up
- Flags-first classifier, precedence correct.
do-reset(rpcErrors.ts:59) precedesconnection(:61) andauth(:66), so a reset frame also carryingoverloaded/retryablestill resolves todo-reset— pinned atrpcErrors.test.ts:48. The round-trip canary closes the one real gap: a capnweb regression dropping custom Error props would fail CI instead of silently demoting everything to message-matching. - Backend stub fix is the reachability fix, and not a regression. Turning the cached stub into a per-call
get user()(server.ts:93) is what lets a retried read hit a fresh incarnation. I checked the field-init order:this.usersis constructor-assigned exactly as before (only theuserparam moved to a getter), and the getter is never invoked during construction — so the refactor is safe. The doc-comment earns its place in the kernel. - Subscribe retry can't leak a subscription. I traced
subscribeConnectedAccounts(user.ts:1342): a reset destroys the incarnation that would hold the subscription, so a retry through the fresh stub re-subscribes to the new incarnation with nothing dangling on a live one. Safe. - Retries confined to idempotent reads/subscribes.
withDoResetRetryrefuses flagless local transport errors (rpcErrors.ts:101), so a retry never fires through a closure-captured dead stub; the connection manager keeps that recovery. Writes keep inline-hint-not-auto-retry, and the double-send reasoning (a reset after commit) is right. - Auth drift closed. Both raw throws migrated to
createAuthError, and the non-Base64 token path (user.ts:302) now classifies as auth instead of leaking the decoder'sSyntaxError. - "Loud on purpose" carve-outs (
Connections.tsx,ObserverConfigModal.tsxsubscribe) correctly keepconsole.error+ toast where there's no retry path.
One new non-blocking observation
ChatInput clears the send-hiccup hint on chatKey change (useEffect(() => setSendHiccup(false), [chatKey])), and chatKey={selectedChatId} is number | null. When the composer is on the "new chat" surface, selectedChatId stays null across two different not-yet-created conversations, so a hiccup hint from a first failed new-chat send would persist into a second new-chat attempt rather than clearing. It's a cosmetic edge (a stale but accurate "may not have been sent — check the thread" warning on the same empty surface), and handleSend resets it on the next send anyway (setSendHiccup(false) at the top), so nothing misfires. Not worth changing; just noting the null-key coalescing since the prior reviews didn't.
Verdict: Solid, well-scoped, and the commit split keeps the kernel changes reviewable apart from the UI. LGTM.
|
Review posted to PR #58. SummaryI independently re-reviewed PR #58 from a clean checkout rather than relying on the three prior What I verified:
One new non-blocking observation the prior reviews missed: Verdict: Solid, well-scoped, mergeable. LGTM. |
There was a problem hiding this comment.
Review — Classify RPC errors and recover quietly from DO resets
I reviewed PR #58 from a fresh checkout, reading the full diff against the base and re-running every check myself rather than relying on the prior ask-bonk reviews.
Verification I ran
- Installed deps with a frozen lockfile, then
vitest run src/rpcErrors.test.ts→ 20/20 pass (both canaries: the transport-string pin and theserialize/deserializecustom-property round-trip atrpcErrors.test.ts:208). buildonworkshop-backend+workshop-shared+workshop-frontend→ all green (covers theget user()refactor, the sharedAUTH_ERROR_*surface, and every touched frontend call site).lint:check→ exit 0 (no new warnings from any file this PR touches; the twono-shadowwarnings inserver.ts:487/517predate this PR).- Grepped for stray raw auth strings → only the
AUTH_ERROR_MESSAGEStable remains; both raw throws migrated tocreateAuthError.
The design holds up
- Flags-first classifier, precedence correct.
do-reset(rpcErrors.ts:59) precedesconnection(:61) andauth(:66), so a reset frame also carryingoverloaded/retryablestill resolves todo-reset— pinned atrpcErrors.test.ts:48. The round-trip canary closes the one real gap: a capnweb regression dropping custom Error props fails CI instead of silently demoting everything to message-matching. - Backend stub fix is the reachability fix, and safe. Turning the cached stub into a per-call
get user()(server.ts:93) is what lets a retried read hit a fresh incarnation. The getter reads the constructor paramuserIdandthis.users(both assigned before any method runs) and is never invoked during construction, so it's not a field-init regression..id.name/.id.toString()are synchronous local reads on a nativeDurableObjectStub, not Cap'n Web stubs, so the fresh-stub-per-access getter has no disposal-leak consequence — only the micro-churn the prior review noted, correctly left alone. - Retries confined to idempotent reads/subscribes.
withDoResetRetryrefuses flagless local transport errors (rpcErrors.ts:101), so a retry never fires through a closure-captured dead stub; the connection manager keeps that recovery. Writes keep inline-hint-not-auto-retry, and the double-send reasoning (a reset after commit) is right. - Auth drift closed. The non-Base64 token path (
user.ts:302) now classifies as auth instead of leaking the decoder'sSyntaxError. The shared additions are all doc-commented per the kernel bar.
New non-blocking observation
The chat-send path (ChatInterface.tsx:5384) fires reportDoResetError("chat.send", …) on every genuine reset, before logRpcFailure decides whether to quiet. So a reset there is simultaneously (a) sent to the external Reporter as a warning, (b) logged at console.debug, and (c) surfaced as the inline sendHiccup hint via the rethrow. That's intentional and fine — but note it makes chat-send the one transient surface that still reports to the external Reporter, whereas the read sites (routes/index, SidebarWorkspaces, __root) deliberately do not report their DO resets (they retry and only logRpcFailure). The asymmetry is defensible (a failed send is user-visible and non-retried, so worth a warning-level signal) — just worth a one-line note so a future reader doesn't "fix" the inconsistency by either dropping the send report or adding reports to the reads. Not worth changing.
Verdict
Solid, well-scoped, mergeable. The kernel change is minimal and correct, retries are correctly confined, and the commit split keeps workshop-backend/workshop-shared reviewable apart from the UI. LGTM.
|
Review posted to PR #58. SummaryI independently reviewed PR #58 (RPC error classification + quiet DO-reset recovery) from a fresh checkout, verified its assumptions myself, and posted a review. What I verified:
Verdict: Solid, well-scoped, mergeable. The kernel change is minimal and correct. One new non-blocking observation the prior reviews missed: chat-send is the one transient surface that still fires LGTM. |
| @validateRpc() | ||
| class AuthenticatedApiImpl extends RpcTarget implements AuthenticatedApi { | ||
| constructor(private ctx: ExecutionContext, private env: Env, | ||
| private user: DurableObjectStub<UserDurableObject>, |
There was a problem hiding this comment.
This is likely a controversial decision.
Its driven by https://developers.cloudflare.com/durable-objects/best-practices/error-handling/ specifically the block
Many exceptions leave the DurableObjectStub in a "broken" state, such that all attempts to send additional requests will just fail immediately with the original exception. To avoid this, you should avoid reusing a DurableObjectStub after it throws an exception. You should instead create a new one for any subsequent requests.
When the the user DO resets e.g. storage timeout, overloaded abort (which is exactly what we saw in the logs) that stub is permanently poisoned. Even if we retry it will fail.
This is not super obvious because the premise does hold for the workspace path: Overseer stubs get re-resolved through the namespace on each open, so a retried openGadget genuinely reaches the restarted object. The UserDO path is the only one where a stub is cached across calls.
This should be cheap and safe: namespace.get(id) is not a network call. Stub creation is local and lazy.
Why not just force a re-load? a reload doesn't avoid the retry; it is the retry, multiplied by everything else and makes blast radius wildly disproportionate. It give a worse UX as well as the entire page resets (as opposed to trying to recover silently where possible)
| // through the closure-captured dead stub could never succeed anyway. Deliberately retries even | ||
| // when `overloaded` is set alongside the reset — the reset destroyed the queue that was | ||
| // overloaded, and one jittered attempt is not a retry loop. Never use for writes. | ||
| export async function withDoResetRetry<T>(fn: () => Promise<T>, delayMs = 1500): Promise<T> { |
There was a problem hiding this comment.
This comment goes more in depth here, but I want get ahead of any guttural reactions: Retry is confined to idempotent reads, bounded to exactly 1 attempt.
Reads the structured flags enhanced_error_serialization already delivers to the browser (durableObjectReset, retryable, overloaded, durableObjectId; semantics per workerd jsg/util.c++ — see MR 238), with message matching as fallback. Nothing consumed these before: every call site treated a transient DO reset like a terminal error.
A reset DO rejects in-flight RPCs while the WebSocket stays healthy, and nothing ever re-fetched — the sidebar, model list, onboarding check, vendor branding, and connected-account subscriptions stayed broken until reload. The object restarts on its next request, so withDoResetRetry retries once after a jittered delay. Reads only; writes are never retried (a reset after commit would double-apply).
Transient failures (DO reset, connection loss) were logged as scary console errors with dead-end toasts at every load site. Route them through logRpcFailure — debug-level for transient, loud otherwise — and skip the toasts, since a reconnect or retry is expected to cure them. Failed chat sends now show an inline composer hint instead of a toast; the wording is hedged because a reset after commit means the message may have landed. DO resets on the send path report through reportDoResetError for telemetry.
Per the DO error-handling docs, flags are the supported contract: - auth strings move to a shared AUTH_ERROR_MESSAGES constant thrown by the backend and imported by the classifier, so they cannot drift - a canary test pins the flagless capnweb transport messages to the installed build, so an upgrade fails in CI rather than in the UX - the workerd reset strings are documented as re-wrap fallback only - withDoResetRetry documents why one jittered retry is safe despite the overloaded flag accompanying reset errors
3e00b45 to
4c70048
Compare
There was a problem hiding this comment.
Review — Classify RPC errors and recover quietly from DO resets
I reviewed PR #58 independently against the base (origin/chore/handle-do-resets, the #56 stack) rather than relying on the prior ask-bonk passes — reading the classifier, the backend get user()/auth-code changes, the shared AUTH_ERROR_* surface, and every touched frontend call site, then re-running the checks myself.
Verification I ran (fresh pnpm install)
vitest run src/rpcErrors.test.ts→ 20/20 pass, including both canaries: the transport-string pin (rpcErrors.test.ts:194) and theserialize/deserializecustom-property round-trip (:208).types:checkonworkshop-backend+workshop-shared+workshop-frontend→ all green (covers theAuthenticatedApiImplgetter refactor, the shared additions, and every call site).
The design holds up
- Flags-first classifier, precedence correct.
do-reset(rpcErrors.ts:59) precedesconnection(:61) andauth(:66), so a reset frame also carryingoverloaded/retryablestill resolves todo-reset— pinned atrpcErrors.test.ts:48. The round-trip canary closes the one real gap: a capnweb regression dropping custom Error props fails CI instead of silently demoting every classification to message-matching. - Backend stub fix is the reachability fix, and safe. The per-call
get user()(server.ts:93) is what lets a retried read reach the restarted incarnation; a session-cached stub stays poisoned per the DO error-handling docs. I checked field-init order:this.usersis constructor-assigned (server.ts:83) anduserIdis a constructor param, and the getter is never invoked during construction, so this is not a field-init regression. All 40+this.userreads are nativeDurableObjectStubaccesses —.id.name/.id.toString()are synchronous local reads and native stubs need no disposal — so minting a fresh stub per access is harmless micro-churn, not a leak. abortAllDurableObjectsis the right test harness. The new integration test (open-gadget-rpc.test.ts:145) reproduces the exact failure: a non-graceful reset on the same socket/sameAuthenticatedApiImpl, which a cached stub could never recover from. The comment correctly rules outevictDurableObject(graceful, never breaks a stub).- Retries confined to idempotent reads/subscribes.
withDoResetRetryrefuses flagless local transport errors (rpcErrors.ts:101), so a retry never fires through a closure-captured dead stub — the connection manager keeps that recovery. Exactly one jittered attempt, never a loop. The subscribe wrapper inObserverConfigModalis leak-safe: the failing attempt returns no stub, the retry's stub is captured (or disposed oncancelled), and a reset destroys the incarnation that would hold the subscription. - Writes correctly not auto-retried. Chat-send keeps the inline
sendHiccuphint (ChatInterface.tsx:3062) with sound double-send reasoning (a reset landing after the write commits). The composer only sets hint state; theonSendhandlers own the logging. - Auth drift closed. Both raw throws migrated to
createAuthError, and the non-Base64 token path (user.ts:300) now classifies as auth instead of leaking the decoder'sSyntaxError. The shared additions are all doc-commented per the kernel bar. - "Loud on purpose" carve-outs (
Connections.tsx:73,ObserverConfigModal.tsx:119) correctly keepconsole.error+ toast where there's no retry path, so a quieted transient can't strand the user on a permanent loader.
Non-blocking observations
ObserverConfigModal.tsx:131wrapsPromise.all([listGatekeeperVendors(), listAddableGatekeepers()])in the retry, so a reset on the second call re-runs both. Both are idempotent metadata reads — harmless, just slightly more work than a per-call wrap. Not worth changing.ChatInputsend-hint key coalescing (ChatInterface.tsx:1845):chatKey={selectedChatId}staysnullacross two different not-yet-created "new chat" conversations, so a hiccup hint could persist between them. Cosmetic — the warning stays accurate andhandleSendclears it on the next send.- Chat-send report asymmetry:
reportDoResetError("chat.send", …)fires to the external Reporter on every genuine reset (ChatInterface.tsx:5384), whereas the retried read sites deliberately don't report. Defensible (a non-retried, user-visible failure is worth a warning-level signal), but worth a one-line note so a future reader doesn't "fix" the inconsistency in either direction.
Verdict
Solid, well-scoped, mergeable. The kernel change is minimal and correct, retries are confined to idempotent reads with a fresh-stub-per-attempt getter that makes them reachable, and the commit split keeps workshop-backend/workshop-shared reviewable apart from the UI. LGTM.
|
Review posted to PR #58. SummaryI independently reviewed PR #58 (RPC error classification + quiet DO-reset recovery) from a fresh What I verified:
Verdict: Solid, well-scoped, mergeable. The kernel change is minimal and correct — the per-call stub getter is what makes retries reachable. I raised three non-blocking observations (Promise.all retry re-runs both reads on partial failure; the |
Stacked on #56.
Routine DO resets (storage timeouts, overload aborts i.e. the stuff we saw in the logs) were surfacing in the frontend as scary terminal errors. The reject frames actually arrive with structured flags (
durableObjectReset,overloaded, etc., thanks toenhanced_error_serialization), but nothing ever read them: every call site was justcatch(console.error).Two details made it worse than it needed to be:
What changed
Testing
Frontend suite + tsc green, backend too. The classifier tests use a reject frame captured from prod.