{"record":{"id":"da64cc79d5aac58b","repo":"toeverything/AFFiNE","slug":"bad-request-da64cc","errorCode":"bad_request","errorMessage":"Invalid space join batch payload.","messagePattern":"Invalid space join batch payload\\.","errorType":"exception","errorClass":"BadRequest","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/sync/gateway.ts","lineNumber":254,"sourceCode":"    'sync.awareness.collect': SyncAwarenessEvent;\n    'sync.awareness.updated': SyncAwarenessEvent & {\n      awarenessUpdate: string;\n    };\n    'sync.permissions.changed': {\n      spaceType: SpaceType;\n      spaceId: string;\n      docId?: string;\n    };\n  }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null;\n}\n\nfunction parseJoinSpaceBatchMessage(message: unknown): JoinSpaceBatchMessage {\n  if (!isRecord(message)) {\n    throw new BadRequest('Invalid space join batch payload.');\n  }\n\n  const { spaces, clientVersion } = message;\n  if (!Array.isArray(spaces) || spaces.length === 0) {\n    throw new BadRequest('Space join batch must not be empty.');\n  }\n  if (spaces.length > MAX_SPACE_JOIN_BATCH_SIZE) {\n    throw new BadRequest(\n      `Space join batch exceeds limit (${MAX_SPACE_JOIN_BATCH_SIZE}).`\n    );\n  }\n  if (typeof clientVersion !== 'string' || clientVersion.length === 0) {\n    throw new BadRequest('Space join batch requires a client version.');\n  }\n\n  const entries = spaces.map((space, index) => {\n    if (!isRecord(space)) {\n      throw new BadRequest(`Invalid space join batch entry at index ${index}.`);","sourceCodeStart":236,"sourceCodeEnd":272,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b6de0ad51b76f3daac2d3d6325369ea623ed7ed4/packages/backend/server/src/core/sync/gateway.ts#L236-L272","documentation":"The space sync WebSocket gateway parses every space-join-batch message with parseJoinSpaceBatchMessage (gateway.ts:254). This variant is the very first check: the incoming message failed isRecord, meaning it is not a plain object (it is a string, number, boolean, array, or null). All later validation (spaces array, clientVersion, entry shape) only runs after this passes.","triggerScenarios":"A client emits the space-join-batch message with a non-object payload: a JSON string that was never parsed, a bare scalar, an array, or null/undefined.","commonSituations":"Hand-rolled WebSocket clients or scripts; accidentally double-encoding the payload (JSON.stringify applied twice, or stringify on a side that also serializes); an older client version sending a different message shape for this event.","solutions":["Send a plain object with the required shape: { spaces: [...], clientVersion: '<version string>' }","Make sure the payload is stringified exactly once — check for JSON.stringify on both the emit side and the transport wrapper","Upgrade the client to the version whose sync protocol matches this gateway"],"exampleFix":"// before: raw/double-encoded payload\nsocket.emit(joinSpaceBatch, JSON.stringify({ spaces }));\n// after: plain object with required fields\nsocket.emit(joinSpaceBatch, { spaces: entries, clientVersion: appVersion });","handlingStrategy":"validation","validationCode":"// validate before emitting over the socket\nconst msg = buildJoinSpaceBatch(pending, appVersion);\nassert(typeof msg === 'object' && msg !== null && !Array.isArray(msg));\nsocket.emit(joinSpaceBatch, msg);","typeGuard":"const isRecord = (v: unknown): v is Record<string, unknown> =>\n  typeof v === 'object' && v !== null;\n\nconst isJoinSpaceBatchMessage = (m: unknown): m is JoinSpaceBatchMessage =>\n  isRecord(m) &&\n  Array.isArray(m.spaces) && m.spaces.length > 0 &&\n  typeof m.clientVersion === 'string' && m.clientVersion.length > 0;","tryCatchPattern":"// bad_request from the gateway is a client bug: log and stop, never retry\ntry {\n  await socket.emitWithAck(joinSpaceBatch, msg);\n} catch (e) {\n  if (e instanceof BadRequest) { // code === 'bad_request'\n    console.error('malformed join batch', e.message);\n    return; // fix the payload, do not resend it unchanged\n  }\n  throw e;\n}","preventionTips":["Type all outbound WS messages with the gateway's own interfaces (JoinSpaceBatchMessage etc.)","Stringify the payload exactly once; assert isRecord on the value handed to emit","Unit-test the serializer against the gateway's parse functions when possible"],"tags":["websocket","sync","validation","payload"],"backgroundTag":"schema-validation-failed","analyzedSha":"b6de0ad51b76f3daac2d3d6325369ea623ed7ed4","analyzedAt":"2026-08-21T18:20:45.039Z","contentChangedAt":"2026-08-21T18:20:45.039Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}