toeverything/AFFiNE · error · BadRequest

bad_request

bad_request

Error message

Invalid space join batch payload.

What it means

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.

Solutions

  1. Send a plain object with the required shape: { spaces: [...], clientVersion: '<version string>' }
  2. Make sure the payload is stringified exactly once — check for JSON.stringify on both the emit side and the transport wrapper
  3. Upgrade the client to the version whose sync protocol matches this gateway

Example fix

// before: raw/double-encoded payload
socket.emit(joinSpaceBatch, JSON.stringify({ spaces }));
// after: plain object with required fields
socket.emit(joinSpaceBatch, { spaces: entries, clientVersion: appVersion });
Defensive patterns

Strategy: validation

Validate before calling

// validate before emitting over the socket
const msg = buildJoinSpaceBatch(pending, appVersion);
assert(typeof msg === 'object' && msg !== null && !Array.isArray(msg));
socket.emit(joinSpaceBatch, msg);

Type guard

const isRecord = (v: unknown): v is Record<string, unknown> =>
  typeof v === 'object' && v !== null;

const isJoinSpaceBatchMessage = (m: unknown): m is JoinSpaceBatchMessage =>
  isRecord(m) &&
  Array.isArray(m.spaces) && m.spaces.length > 0 &&
  typeof m.clientVersion === 'string' && m.clientVersion.length > 0;

Try / catch

// bad_request from the gateway is a client bug: log and stop, never retry
try {
  await socket.emitWithAck(joinSpaceBatch, msg);
} catch (e) {
  if (e instanceof BadRequest) { // code === 'bad_request'
    console.error('malformed join batch', e.message);
    return; // fix the payload, do not resend it unchanged
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b6de0ad51b (2026-08-21). Data as JSON: /api/errors/da64cc79d5aac58b. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/core/sync/gateway.ts:254

    'sync.awareness.collect': SyncAwarenessEvent;
    'sync.awareness.updated': SyncAwarenessEvent & {
      awarenessUpdate: string;
    };
    'sync.permissions.changed': {
      spaceType: SpaceType;
      spaceId: string;
      docId?: string;
    };
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null;
}

function parseJoinSpaceBatchMessage(message: unknown): JoinSpaceBatchMessage {
  if (!isRecord(message)) {
    throw new BadRequest('Invalid space join batch payload.');
  }

  const { spaces, clientVersion } = message;
  if (!Array.isArray(spaces) || spaces.length === 0) {
    throw new BadRequest('Space join batch must not be empty.');
  }
  if (spaces.length > MAX_SPACE_JOIN_BATCH_SIZE) {
    throw new BadRequest(
      `Space join batch exceeds limit (${MAX_SPACE_JOIN_BATCH_SIZE}).`
    );
  }
  if (typeof clientVersion !== 'string' || clientVersion.length === 0) {
    throw new BadRequest('Space join batch requires a client version.');
  }

  const entries = spaces.map((space, index) => {
    if (!isRecord(space)) {
      throw new BadRequest(`Invalid space join batch entry at index ${index}.`);

View on GitHub (pinned to b6de0ad51b)