{"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":"validation","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/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/mail/resolver.ts#L132-L168","documentation":"BadRequest thrown by normalizeMailWindow when the analytics window hours param is present and not 24 or 168 (24*7). The mail-delivery analytics resolver only supports a 1-day or 7-day window; any other value (including 48, 72, 12) is rejected. Correctly typed as BadRequest.","triggerScenarios":"Admin panel mail analytics query with hours=48, hours=12, or any value other than 24/168/undefined. Default (undefined) falls back to 24.","commonSituations":"A new dashboard widget offering an arbitrary hour picker instead of a fixed day/week toggle. API client built against docs that didn't note the constraint. Refactor that changed the param from a window enum to a raw number.","solutions":["Constrain the UI to two options: '24h' and '7d' (hours=24 / hours=168), or 'unset' for the default.","If you need more windows, extend normalizeMailWindow and the bucketing logic (the bucket switches at >24) — don't just widen the guard, the bucket math assumes 24 or 168.","Validate client-side before submitting the query.","Return allowed values in the error message for discoverability."],"exampleFix":"// before\nif (hours !== undefined && hours !== 24 && hours !== 24 * 7) {\n  throw new BadRequest('Mail delivery analytics window must be 24 or 168 hours.');\n}\n\n// after — accept a typed window and document it\ntype MailWindow = 24 | 168;\nfunction normalizeMailWindow(hours: MailWindow | undefined = 24) {\n  if (hours !== 24 && hours !== 168) {\n    throw new BadRequest('Mail delivery analytics window must be 24 or 168 hours.');\n  }\n  ...\n}","handlingStrategy":"validation","validationCode":"type MailWindow = 24 | 168;\nfunction assertMailWindow(hours: unknown): asserts hours is MailWindow | undefined {\n  if (hours !== undefined && hours !== 24 && hours !== 168) {\n    throw new UserError('Mail window must be 24 or 168 hours');\n  }\n}","typeGuard":"function isValidMailWindow(hours: unknown): hours is 24 | 168 | undefined {\n  return hours === undefined || hours === 24 || hours === 168;\n}","tryCatchPattern":"if (!isValidMailWindow(hours)) {\n  return res.status(400).send('Use 24 or 168 hours');\n}\nawait resolver.adminMailDeliveries({ hours });","preventionTips":["Offer only '24h' and '7d' toggles in the UI.","Type the param as a literal union so invalid values are rejected at compile time.","If you need more windows, extend the bucket math, not just the guard."],"tags":["mail","analytics","validation","input-validation","admin"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}