Skip to content

Commit c3756ff

Browse files
claude[bot]claude
andauthored
docs(realtime): flatten WebSocket error fences onto ErrorMessageSchema (#17186)
`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. Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU Co-authored-by: Claude <noreply@anthropic.com>
1 parent 65444d5 commit c3756ff

1 file changed

Lines changed: 41 additions & 29 deletions

File tree

content/docs/protocol/kernel/realtime-protocol.mdx

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -263,14 +263,21 @@ Subscribe to changes on a specific object:
263263
```
264264

265265
**Error Response:**
266+
267+
Every `type: "error"` message on this page is one `ErrorMessageSchema` envelope
268+
(`packages/spec/src/api/websocket.zod.ts`): `messageId`, `type` and `timestamp` from
269+
`BaseWebSocketMessage`, then a **top-level** `code` and `message`, plus an optional `details` bag
270+
for anything else. There is no nested `error` object — a client that reads `msg.error.code` gets
271+
`undefined`.
272+
266273
```json
267274
{
275+
"messageId": "550e8400-e29b-41d4-a716-446655440000",
268276
"type": "error",
269-
"subscription_id": "sub_1",
270-
"error": {
271-
"code": "FORBIDDEN",
272-
"message": "No permission to subscribe to 'task' object"
273-
}
277+
"timestamp": "2024-01-16T14:30:00Z",
278+
"code": "FORBIDDEN",
279+
"message": "No permission to subscribe to 'task' object",
280+
"details": { "subscription_id": "sub_1" }
274281
}
275282
```
276283

@@ -667,13 +674,12 @@ Each connection consumes server resources.
667674
**Exceeding limits:**
668675
```json
669676
{
677+
"messageId": "6f1c2f3a-8b7d-4a1e-9c22-1a5f0d2b7e44",
670678
"type": "error",
671-
"error": {
672-
"code": "TOO_MANY_SUBSCRIPTIONS",
673-
"message": "Maximum 50 subscriptions per connection",
674-
"current": 50,
675-
"max": 50
676-
}
679+
"timestamp": "2024-01-16T14:30:00Z",
680+
"code": "TOO_MANY_SUBSCRIPTIONS",
681+
"message": "Maximum 50 subscriptions per connection",
682+
"details": { "current": 50, "max": 50 }
677683
}
678684
```
679685

@@ -1022,20 +1028,20 @@ Handle JWT expiration gracefully:
10221028

10231029
```json
10241030
{
1031+
"messageId": "9a3f5d61-2c40-4f7b-b8e1-73d5a6c9f012",
10251032
"type": "error",
1026-
"error": {
1027-
"code": "TOKEN_EXPIRED",
1028-
"message": "JWT token expired",
1029-
"expires_at": "2024-01-16T14:30:00Z"
1030-
}
1033+
"timestamp": "2024-01-16T14:30:00Z",
1034+
"code": "TOKEN_EXPIRED",
1035+
"message": "JWT token expired",
1036+
"details": { "expires_at": "2024-01-16T14:30:00Z" }
10311037
}
10321038
```
10331039

10341040
**Client response:** Refresh token and reconnect
10351041
```javascript
10361042
ws.onmessage = async (event) => {
10371043
const msg = JSON.parse(event.data);
1038-
if (msg.error?.code === 'TOKEN_EXPIRED') {
1044+
if (msg.type === 'error' && msg.code === 'TOKEN_EXPIRED') {
10391045
const newToken = await refreshToken();
10401046
ws.close();
10411047
reconnect(newToken);
@@ -1044,25 +1050,31 @@ ws.onmessage = async (event) => {
10441050
```
10451051

10461052
### Rate Limiting
1047-
WebSocket messages are rate-limited:
1053+
1054+
<Callout type="warn">
1055+
**Not implemented.** Nothing rate-limits WebSocket messages today. No producer anywhere in this
1056+
repository emits `RATE_LIMITED` on a realtime path, and there is no WebSocket transport for such
1057+
a message to arrive on (see the implementation-status callout at the top of this page). The
1058+
budget below is a **planned** one, and the envelope beside it is what `ErrorMessageSchema` would
1059+
require of a producer that eventually enforces it — not a shape any code emits today. HTTP
1060+
requests *are* rate-limited, under a different code and envelope: see
1061+
[Error Handling](/docs/protocol/kernel/error-handling) for `RATE_LIMIT_EXCEEDED`.
1062+
</Callout>
1063+
1064+
**Planned limits (not enforced):**
1065+
- Max 100 messages per minute per connection
1066+
- Max 10 subscriptions per minute per connection
10481067

10491068
```json
10501069
{
1070+
"messageId": "c4e7b9d2-5a31-4c8f-9e60-2b8d41f7a305",
10511071
"type": "error",
1052-
"error": {
1053-
"code": "RATE_LIMITED",
1054-
"message": "Too many messages",
1055-
"retry_after": 5,
1056-
"limit": 100,
1057-
"window": "1m"
1058-
}
1072+
"timestamp": "2024-01-16T14:30:00Z",
1073+
"code": "RATE_LIMITED",
1074+
"message": "Too many messages"
10591075
}
10601076
```
10611077

1062-
**Limits:**
1063-
- Max 100 messages per minute per connection
1064-
- Max 10 subscriptions per minute per connection
1065-
10661078
## Next Steps
10671079

10681080
<Cards>

0 commit comments

Comments
 (0)