{"record":{"id":"ae2716d106a1adfe","repo":"toeverything/AFFiNE","slug":"bad-request-ae2716","errorCode":"bad_request","errorMessage":"Invalid origin: ${origin}, referer: ${referer}","messagePattern":"Invalid origin: (.+?), referer: (.+?)","errorType":"exception","errorClass":"BadRequest","httpStatus":400,"severity":"warning","filePath":"packages/backend/server/src/core/telemetry/gateway.ts","lineNumber":37,"sourceCode":"type EventResponse<Data = any> = [Data] extends [never]\n  ? { data?: never }\n  : { data: Data };\n\n@WebSocketGateway()\n@UseInterceptors(ClsInterceptor)\nexport class TelemetryGateway {\n  constructor(private readonly telemetry: TelemetryService) {}\n\n  @SubscribeMessage('telemetry:batch')\n  async onBatch(\n    @CurrentUser() user: CurrentUser,\n    @ConnectedSocket() client: Socket,\n    @MessageBody() batch: TelemetryBatch\n  ): Promise<EventResponse<TelemetryAck>> {\n    const origin = client.handshake.headers.origin;\n    const referer = client.handshake.headers.referer;\n    if (!this.telemetry.isOriginAllowed(origin, referer)) {\n      throw new BadRequest(`Invalid origin: ${origin}, referer: ${referer}`);\n    }\n\n    const ack = await this.telemetry.collectBatch({\n      ...batch,\n      transport: 'ws',\n      events: batch?.events?.map(event => ({\n        ...event,\n        userId: event.userId ?? user?.id,\n      })),\n    });\n\n    return { data: ack };\n  }\n}\n","sourceCodeStart":19,"sourceCodeEnd":52,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/core/telemetry/gateway.ts#L19-L52","documentation":"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.","triggerScenarios":"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.","commonSituations":"App served from a new domain after config drift; embedding the app on another site; proxies rewriting handshake headers during the upgrade request.","solutions":["Allow-list the origin the page is served from (telemetry origins and/or url.allowedOrigins config)","Verify the handshake actually carries the expected Origin/Referer (proxy inspection)","Non-browser clients should connect without spoofing an Origin, or add their declared origin to the allow-list"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// gate ws telemetry emission on the handshake origin\nconst origin = new URL(socket.io.uri, location.href).origin;\nif (!allowedOrigins.includes(origin)) {\n  socket.off('telemetry:batch'); // avoid guaranteed rejections\n}","typeGuard":"function isHandshakeAllowed(handshakeOrigin: string | undefined, allowed: string[]): boolean {\n  return !handshakeOrigin || allowed.includes(handshakeOrigin);\n}","tryCatchPattern":null,"preventionTips":["Allow-list every origin that opens telemetry sockets in server config","Non-browser ws clients should not set an Origin header unless it is allow-listed","Check handshake headers at the proxy to catch rewrites before they reach the gateway"],"tags":["cors","telemetry","websocket","origin"],"backgroundTag":"cors-origin-rejected","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}