docs(realtime): flatten the WebSocket error fences onto the declared ErrorMessageSchema - #17186
Merged
Merged
Conversation
`ErrorMessageSchema` (packages/spec/src/api/websocket.zod.ts) is the only schema on this tree governing a `type: "error"` WebSocket message, and it is flat: `messageId` / `type` / `timestamp` from `BaseWebSocketMessage`, then top-level `code` and `message`, plus an optional `details` bag. The realtime protocol page wrapped those fields in a nested `error` object in four fences, so a client written from any of them reads `msg.error.code` and gets `undefined`. Flatten all four (subscribe FORBIDDEN, TOO_MANY_SUBSCRIPTIONS, TOKEN_EXPIRED, RATE_LIMITED) onto the declared envelope, move the extra per-fence context into `details`, and fix the one consumer snippet that destructured the nested shape. Mark the WebSocket rate-limiting section as not implemented: no producer emits `RATE_LIMITED` on any realtime path and there is no WebSocket transport to carry it, so the stated per-connection budget is planned, not enforced. The `type: "auth_error"` fence is deliberately left alone: `auth_error` is not a member of `WebSocketMessageType`, so `ErrorMessageSchema` does not govern it and rewriting it would change the protocol rather than the prose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
baozhoutao
marked this pull request as ready for review
September 9, 2026 15:34
baozhoutao
enabled auto-merge
September 9, 2026 15:34
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.
Fixes #16935
Clause-②: no — the diff is
content/docs/**only; no published package'sfiles[]shipscontent/, so the published surface is byte-unchanged (measured: 70 non-private packages, zero whosefiles[]namescontent).What was wrong
ErrorMessageSchema(packages/spec/src/api/websocket.zod.ts:365) is the only schema on this tree that governs atype: "error"WebSocket message, and it is flat —messageId/type/timestampfromBaseWebSocketMessage(:269), then a top-levelcodeandmessage, plus an optionaldetailsbag.content/docs/protocol/kernel/realtime-protocol.mdxwrappedcodeandmessagein a nestederrorobject in four fences, so a client written from any of them readsmsg.error.codeand getsundefinedon every realtime error message.Per the dispatch ruling on the card: the two readings the card proposed do not lead to opposite edits for the envelope half — even if WebSocket message rate limiting shipped tomorrow it would emit a flat message, because that schema is the only declaration of the message type. The nested shape is wrong under both readings.
The four fences, flattened
:268FORBIDDENsubscription_idmoved intodetails:670TOO_MANY_SUBSCRIPTIONScurrent/maxmoved intodetails:1025TOKEN_EXPIREDexpires_atmoved intodetails:1051RATE_LIMITEDretry_after/limit/windowdropped (see below)Field spellings inside
detailsare relocated verbatim, not renamed — no key on this page was invented or normalised. One consumer snippet was destructuring the nested shape and is corrected in the same hunk:Verification — every fence parsed against the built schema
Not a gate (there is none for a JSON fence), so this is a one-off run, before and after, against
packages/spec/dist/api/index.mjsafter a real build. Script lived in the scratchpad and is not checked in.4/4 pass after, 4/4 fail before, same script and same built schema for both legs — so the pass is a measurement, not a script that stopped looking. The
messageIdvalues are real v4-shaped UUIDs;z.string().uuid()is what rejects them otherwise.Rate limiting — marked not implemented, no shape invented
Per the dispatch: the section is now behind a
Callout type="warn"saying nothing rate-limits WebSocket messages today, and the prose budget is relabelled Planned limits (not enforced). No producer emitsRATE_LIMITEDon any realtime path, and there is no WebSocket transport for such a message to arrive on —packages/spec/CHANGELOG.mdrecords that "no WebSocket server is mounted (#2462); the protocol is a future wire contract", which the page's own top-of-page callout already says.The callout also points at the HTTP surface, which does have a live rate-limit shape under a different code (
RATE_LIMIT_EXCEEDED,details.retryAfterSeconds) — that being the reason #15855 carved this page out by construction rather than by preference.Readings, remeasured on
fd5cff209f(file counts overpackages/**):RATE_LIMITEDRATE_LIMIT_EXCEEDEDreturns 15 filesauth_errorat:220— measured, deliberately untouched, filed separatelyThe dispatch fenced this one out and asked for a measurement rather than a rewrite. Measured live against the built spec:
(
WebSocketMessageType.safeParse(t)over the builtdist;erroris the positive control on the same corpus and the same call.) Grep legs on the same tree:auth_erroroverpackages/**returns one line and it is not a hit —packages/spec/src/identity/protocol.ts:136isOAUTH_ERROR: 'oauth_error', matched as a substring;auth_successreturns zero across the whole repo outsidecontent/docs. Positive control for both:ErrorMessageSchemareturns 11 lines across 6 files.So
auth_errorhas no declaration. Rewriting the fence totype: "error"would change the protocol rather than the prose, so it is left byte-unchanged here. The finding turned out to be wider than one fence —auth,auth_success,auth_errorandsubscribedare all outsideWebSocketMessageType, and unlike this card's envelope half the two readings there genuinely do diverge (fix the page vs. grow the enum) — so it is filed as #17184 rather than folded in.Gates
node scripts/pm/dispatch-gates.mjs --commandsderived 39 families for this diff; all 39 were run and all 39 exited 0. Reconciliation:Four of them first returned
PREREQUISITE NOT MET/ not-built (check:doc-formula-expressions,check:doc-security-posture,check:skill-examples,check:docs-transcript-drift) — nothing measured, not failures. They were re-run to a real verdict afterturbo run build --filter='@objectstack/lint...' --filter=@objectstack/client-react --filter=@objectstack/clientandpnpm --filter @objectstack/spec build, both throughscripts/pm/os-verify-lock.sh(VERDICT command-exit 0on each). Exit codes were captured by redirect before any pipe.No package build closure is owed: the diff touches no package, so there is no
'<pkg>^...'closure and no packagetest/typecheckin scope.packages/specwas built only because four of the derived gates read itsdist.pnpm lint(eslint . --no-inline-config) is CI's whole-repo run; the diff is a single.mdxfile, which that config does not lint, so nothing was narrowed away here.Not in scope, not touched
:122—"message": "In-process event bus only …"is a field of a capabilities response, not an error envelope. Left alone, as ruled.subscription_id,object,events,filteron the subscribe pair) — same population as [finding] realtime-protocol.mdx documents four WebSocket message types thatWebSocketMessageTypedoes not declare (auth,auth_success,auth_error,subscribed) #17184, noted there.content/docs/protocol/kernel/error-handling.mdxis a different file dispatched concurrently on [finding] the 429 details fences in error-handling.mdx teach three members no producer emits, and omit the one it does #16937; [finding] the 429 details fences in error-handling.mdx teach three members no producer emits, and omit the one it does #16937 is not addressed here and remains open.Generated by Claude Code