{"record":{"id":"db9a9cb65b3d66cf","repo":"toeverything/AFFiNE","slug":"bad-request-db9a9c","errorCode":"bad_request","errorMessage":"Mail delivery analytics window must be 24 or 168 hours.","messagePattern":"Mail delivery analytics window must be 24 or 168 hours\\.","errorType":"exception","errorClass":"BadRequest","httpStatus":400,"severity":"warning","filePath":"packages/backend/server/src/core/mail/resolver.ts","lineNumber":150,"sourceCode":"}\n\nfunction startOfUtcDay(value: Date) {\n  return new Date(\n    Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate())\n  );\n}\n\nfunction addUtcHours(value: Date, hours: number) {\n  return new Date(value.getTime() + hours * 60 * 60 * 1000);\n}\n\nfunction addUtcDays(value: Date, days: number) {\n  return new Date(value.getTime() + days * 24 * 60 * 60 * 1000);\n}\n\nfunction normalizeMailWindow(hours: number | undefined) {\n  if (hours !== undefined && hours !== 24 && hours !== 24 * 7) {\n    throw new BadRequest(\n      'Mail delivery analytics window must be 24 or 168 hours.'\n    );\n  }\n  const requestedHours = hours ?? 24;\n  const bucket = requestedHours > 24 ? ('day' as const) : ('hour' as const);\n  const now = new Date();\n  const to =\n    bucket === 'hour'\n      ? addUtcHours(startOfUtcHour(now), 1)\n      : addUtcDays(startOfUtcDay(now), 1);\n  const effectiveSize =\n    bucket === 'hour' ? requestedHours : requestedHours / 24;\n  const from =\n    bucket === 'hour'\n      ? addUtcHours(to, -effectiveSize)\n      : addUtcDays(to, -effectiveSize);\n\n  return {","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/core/mail/resolver.ts#L132-L168","documentation":"normalizeMailWindow validates the hours argument of the admin mail-delivery analytics query: only undefined (defaults to 24), 24, or 168 are accepted; anything else throws BadRequest. The value also selects the bucket: 24h→hourly buckets, 168h→daily buckets.","triggerScenarios":"Querying adminMailDeliveries with hours=48, 12, or 720; a client sending a string like '24' or an interval like '7d' where a number is required; a UI slider that allows arbitrary day ranges instead of the two fixed windows.","commonSituations":"Custom admin dashboards built on the GraphQL API assuming any window is supported; refactors that started computing hours client-side (e.g. diff between two dates) instead of using the two presets.","solutions":["Pass exactly 24, 168, or omit the argument","Fix the client to offer only the two supported windows (24h / 7d) instead of a free-form range picker","Validate/coerce the input to a number before sending the GraphQL request","If you need other windows, that requires a backend change — do not try to work around per-request"],"exampleFix":"// before\nadminMailDeliveries({ input: { hours: windowDays * 24 } });\n\n// after\nconst hours = windowDays === 7 ? 168 : 24; // only 24 and 168 are valid\nadminMailDeliveries({ input: { hours } });","handlingStrategy":"validation","validationCode":"const hours = input?.hours;\nif (hours !== undefined && hours !== 24 && hours !== 168) {\n  throw new Error('hours must be 24, 168, or omitted');\n}\nawait adminMailDeliveries({ input: { hours } });","typeGuard":"function isInvalidMailWindow(e: unknown): boolean {\n  const err = e as { extensions?: { code?: string }; message?: string };\n  return err.extensions?.code === 'bad_request' && /24 or 168/.test(err.message ?? '');\n}","tryCatchPattern":"try {\n  const stats = await adminMailDeliveries({ input });\n} catch (e) {\n  if (isInvalidMailWindow(e)) return loadDefault24h(); // fall back to the default window\n  throw e;\n}","preventionTips":["Offer only 'last 24h' and 'last 7d' presets in admin dashboards","Coerce and range-check the hours argument client-side before the GraphQL call","Keep a shared constants module for the supported analytics windows"],"tags":["mail","analytics","admin","graphql","validation"],"backgroundTag":"invalid-query-parameter","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"}