toeverything/AFFiNE · warning · BadRequest

bad_request

bad_request

Error message

Invalid origin: ${origin}, referer: ${referer}

What it means

The WebSocket telemetry gateway applies the identical origin allow-list check to the socket handshake headers before accepting a 'telemetry:batch' event. Browsers always attach an Origin to the handshake, so a page loaded from a non-allow-listed domain gets every telemetry batch rejected with bad_request.

Solutions

  1. Allow-list the origin the page is served from (telemetry origins and/or url.allowedOrigins config)
  2. Verify the handshake actually carries the expected Origin/Referer (proxy inspection)
  3. Non-browser clients should connect without spoofing an Origin, or add their declared origin to the allow-list
Defensive patterns

Strategy: validation

Validate before calling

// gate ws telemetry emission on the handshake origin
const origin = new URL(socket.io.uri, location.href).origin;
if (!allowedOrigins.includes(origin)) {
  socket.off('telemetry:batch'); // avoid guaranteed rejections
}

Type guard

function isHandshakeAllowed(handshakeOrigin: string | undefined, allowed: string[]): boolean {
  return !handshakeOrigin || allowed.includes(handshakeOrigin);
}

Prevention

When it happens

Trigger: Emitting 'telemetry:batch' from a page whose handshake Origin is not in allowedOrigins; a Referer whose origin is unlisted when Origin is absent; server-side ws clients forging an Origin header that is not allow-listed.

Common situations: App served from a new domain after config drift; embedding the app on another site; proxies rewriting handshake headers during the upgrade request.

Understand the failure class

Background: BAD_REQUEST error code: request rejected as invalid (HTTP 400) - causes and fixes across libraries — this error's family across 8 libraries.

Related errors


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

Appendix: source

Thrown at packages/backend/server/src/core/telemetry/gateway.ts:37

type EventResponse<Data = any> = [Data] extends [never]
  ? { data?: never }
  : { data: Data };

@WebSocketGateway()
@UseInterceptors(ClsInterceptor)
export class TelemetryGateway {
  constructor(private readonly telemetry: TelemetryService) {}

  @SubscribeMessage('telemetry:batch')
  async onBatch(
    @CurrentUser() user: CurrentUser,
    @ConnectedSocket() client: Socket,
    @MessageBody() batch: TelemetryBatch
  ): Promise<EventResponse<TelemetryAck>> {
    const origin = client.handshake.headers.origin;
    const referer = client.handshake.headers.referer;
    if (!this.telemetry.isOriginAllowed(origin, referer)) {
      throw new BadRequest(`Invalid origin: ${origin}, referer: ${referer}`);
    }

    const ack = await this.telemetry.collectBatch({
      ...batch,
      transport: 'ws',
      events: batch?.events?.map(event => ({
        ...event,
        userId: event.userId ?? user?.id,
      })),
    });

    return { data: ack };
  }
}

View on GitHub (pinned to b4c8548c09)