fix(backend): fail fast on disconnected sessions + honest error taxonomy#5
Merged
Conversation
Closes the three QA follow-ups from the attended M3 e2e, hardened further
by this round's review lanes:
- No-extension fail-fast: CfSessionCoordinator.send() rejects immediately
unless the host reports a live, onConnect-authorized extension socket
(CoordinatorHost.hasAuthorizedConnection). The gate reads that delivery
predicate - the exact precondition sendToExtension relies on - NOT the
persisted SessionState.status scalar: a late onClose from a replaced
socket stamps "detached" over a healthy reconnect, which would wedge a
live session into permanent 503 (architect-lane catch). onClose likewise
only detaches when no other authorized socket remains.
- Error taxonomy at the commands route, mapped by message-prefix constants
(src/coordinator.ts - the only failure signal that survives the DO RPC
boundary): 503 {extension not connected} (non-2xx on purpose: a 200
ok:false Event would be cached by consumers' idempotency stores and
replayed after reconnect), 504 {command timed out}, 400 {invalid body}
for unparseable JSON, uniform JSON 500 via app.onError for the rest.
Ref-less dryRun writes keep their documented zero-wire simulated-ok
semantic (never a liveness signal) - regression-tested.
- fill_secret gates on the same predicate BEFORE resolveSecret: no vault
read and no plaintext materialization for a command that cannot
dispatch (DL-004).
- Dry-run probe failures surface the extension's own reason
("dry-run: stale or unknown ref: s1e9"; "unexpected probe response
'<type>'") instead of one collapsed string.
- Env.CALLER_TOKENS/EXTENSION_TOKENS are required types - they were
always in wrangler.jsonc secrets.required; the optional typing lied.
auth.ts keeps the empty-string runtime guards.
Tests 53 -> 64: fail-fast unit + adversarial liveness (full
pending->connected->detached->reconnected lifecycle; a replaced socket's
close is not a detach), vault-untouched fill_secret gate, probe-reason
fidelity, 400/500 route tests, and the old 35s DO-timeout test converted
into the route-level 504 proof (same wall-clock, full route->RPC->DO
path, marker-clear re-asserted). Workspace: 142 tests green.
Known noise: fail-fast rejections log "Uncaught (in promise)" via
workerd's DO RPC instrumentation in vitest/wrangler tail - these are
handled 503s, not real uncaught errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the three pre-existing QA follow-ups from the attended M3 e2e, plus the hardening the review lanes drove on top. Pure backend change; protocol/extension/connector untouched.
1. No-extension fail-fast (503, was a 30s-timeout 500)
CfSessionCoordinator.send()now rejects immediately unless the host reports a live, onConnect-authorized extension socket (CoordinatorHost.hasAuthorizedConnection()), and the commands route maps that to503 {error:"extension not connected"}.Two load-bearing details:
SessionState.status. The status scalar is a lossy last-writer-wins echo: a lateonClosefrom a replaced socket stampsdetachedover a healthy reconnected session, which would wedge it into permanent 503 with no backstop (architect-lane catch).onClosenow also only detaches when no other authorized socket remains.ok:falseEvent would be cached by consumers' breakwater idempotency stores and replayed even after the extension reconnects; a 503 makes@understudy/connector'scallUnderstudythrow, releasing the reservation so the retry re-executes.Ref-less dryRun writes (e.g. navigate) keep their documented zero-wire simulated-
oksemantic — regression-tested.2. Honest error taxonomy at the route
Mapped by message-prefix constants in the portable
src/coordinator.ts(a prefix is the only failure signal that survives the DO RPC boundary): 503 not-connected · 504{error:"command timed out"}· 400{error:"invalid body"}for unparseable JSON (was a masked 500) · uniform JSON 500 viaapp.onErrorfor everything else.3. The rest
fill_secretchecks the predicate beforeresolveSecret— no vault read / plaintext materialization for an undeliverable command (DL-004), spy-tested.dry-run: stale or unknown ref: s1e9,unexpected probe response '<type>') instead of one collapsed string.Env.CALLER_TOKENS/EXTENSION_TOKENSare now required types (they were always insecrets.required;wrangler deploy --dry-runvalidates clean). Runtime empty-string guards stay.Test plan
pending→connected→detached→reconnected; replaced-socket close ≠ detach; vault-untouched disconnectedfill_secret; probe-reason fidelity incl. wrong-event-type branch; 400/500 route tests; the old 35s DO-timeout test converted into the route-level 504 proof (same wall-clock cost, full route→RPC→DO path, awaiting-marker clear re-asserted)pnpm -r typecheck(4/4) +pnpm -r test= 142 passedNote for
wrangler tailreaders: fail-fast rejections printUncaught (in promise)via workerd's DO RPC instrumentation — these are handled 503s, not real uncaught errors.🤖 Generated with Claude Code