{"record":{"id":"c43051bb0b956960","repo":"toeverything/AFFiNE","slug":"bad-request-c43051","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/controller.ts","lineNumber":25,"sourceCode":"  type CurrentUser as CurrentUserType,\n  Public,\n} from '../auth';\nimport { TelemetryService } from './service';\nimport { TelemetryAck, type TelemetryBatch } from './types';\n\n@Public()\n@UseNamedGuard('version')\n@Throttle('default')\n@Controller('/api/telemetry')\nexport class TelemetryController {\n  constructor(private readonly telemetry: TelemetryService) {}\n\n  @Options('/collect')\n  collectOptions(@Req() req: Request, @Res() res: Response) {\n    const origin = req.headers.origin;\n    const referer = req.headers.referer;\n    if (!this.telemetry.isOriginAllowed(origin, referer)) {\n      throw new BadRequest(`Invalid origin: ${origin}, referer: ${referer}`);\n    }\n\n    return res\n      .status(200)\n      .header({\n        ...this.telemetry.getCorsHeaders(origin),\n        'Access-Control-Allow-Methods': 'POST, OPTIONS',\n        'Access-Control-Allow-Headers': 'Content-Type, x-affine-version',\n      })\n      .send();\n  }\n\n  @Post('/collect')\n  async collect(\n    @Req() req: Request,\n    @Res({ passthrough: true }) res: Response,\n    @Body() batch: TelemetryBatch,\n    @CurrentUser() user?: CurrentUserType","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/core/telemetry/controller.ts#L7-L43","documentation":"The CORS preflight for POST /api/telemetry/collect validates the Origin header (falling back to the Referer's origin) against the server's allow-list, built from telemetry config origins plus the deployment's url.allowedOrigins. If either header is present but not allow-listed, the OPTIONS request fails with bad_request; requests carrying neither header are allowed through.","triggerScenarios":"OPTIONS /api/telemetry/collect with an Origin not present in allowedOrigins; a Referer whose URL origin is not allow-listed when Origin is absent; health checks or curl scripts that manually send a foreign Origin header.","commonSituations":"Self-hosting AFFiNE under an extra domain missing from config; embedding the web app on a third-party site; reverse proxies rewriting the Origin header; staging domains not added to the allow-list.","solutions":["Add the serving origin to the telemetry allowed origins (and/or the deployment url.allowedOrigins) in server config","Serve the app from the exact domain clients use so Origin matches the allow-list","Fix the reverse proxy to pass the original Origin and Referer headers through unmodified"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// in the browser: only send telemetry when our origin is allow-listed\nconst { origins } = await fetch('/api/telemetry/allowed-origins').then(r => r.json());\nconst allowed = origins.includes(window.location.origin);","typeGuard":"function isOriginAllowed(origin: string, allowedOrigins: string[]): boolean {\n  return allowedOrigins.includes(origin);\n}","tryCatchPattern":null,"preventionTips":["Register every domain that serves the app (including staging and mirrors) in the telemetry/url allowed origins config","Do not strip or rewrite Origin/Referer at the reverse proxy","Remember the rule is asymmetric: no Origin AND no Referer passes, any present-but-unknown origin fails"],"tags":["cors","telemetry","http","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"}