{"record":{"id":"3939136f631e6ce4","repo":"koala73/worldmonitor","slug":"at-least-one-of-domain-or-country-is-required","errorCode":null,"errorMessage":"At least one of domain or country is required","messagePattern":"At least one of domain or country is required","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/intelligence/v1/get-intel-timeline.ts","lineNumber":48,"sourceCode":" * (ApiError, surfaced by server/error-mapper.ts) tells the caller their\n * request was wrong instead of blaming the backend. buf.validate cannot\n * express \"at least one of\", so the check lives in code.\n *\n * A store failure returns an empty result with `upstreamUnavailable: true`\n * rather than a 5xx, so the gateway does not cache an empty timeline for the\n * slow tier's TTL. Premium-gated at the gateway via PREMIUM_RPC_PATHS +\n * ENDPOINT_ENTITLEMENTS.\n */\nexport const getIntelTimeline: IntelligenceServiceHandler['getIntelTimeline'] = async (\n  _ctx: ServerContext,\n  req: GetIntelTimelineRequest,\n): Promise<GetIntelTimelineResponse> => {\n  // Normalize BEFORE the scope check: a whitespace-only country would\n  // otherwise satisfy \"at least one scope\" and then match nothing.\n  const scope = validateHistoryScope(req, MAX_LIMIT);\n  const { domain, country } = scope;\n  if (!domain && !country) {\n    throw new ApiError(400, 'At least one of domain or country is required', '');\n  }\n\n  const limit = resolveLimit(scope.limit, DEFAULT_LIMIT, MAX_LIMIT);\n  const result = await cacheSuccessfulHistoryRead('timeline', {\n    domain, country, from: scope.from, to: scope.to, limit,\n  }, () => intelHistoryTimeline({ ...scope, limit }));\n  if (!result) {\n    return { records: [], partial: false, upstreamUnavailable: true };\n  }\n\n  return { records: result.records, partial: result.partial, upstreamUnavailable: false };\n};\n","sourceCodeStart":30,"sourceCodeEnd":61,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/intelligence/v1/get-intel-timeline.ts#L30-L61","documentation":"getIntelTimeline throws ApiError 400 when, after validateHistoryScope normalization, both scope.domain and scope.country are empty. Normalization runs BEFORE this check specifically so a whitespace-only country cannot satisfy the 'at least one scope' requirement and then silently match nothing. The endpoint is premium-gated at the gateway, so callers reaching this error already passed entitlement.","triggerScenarios":"Calling /api/intelligence/v1/get-intel-timeline with no domain and no country; sending country: '   ' (whitespace only) and nothing else; sending only from/to date range and limit, assuming dates alone scope the query.","commonSituations":"UI 'clear filters' action submits the form with every scope field emptied; API explorer request built without copying the scope parameter; client treats country as optional and domain as optional without enforcing one-of.","solutions":["Supply a non-empty domain or country (or both) on every call","Enforce the one-of rule in the client: if both filters are cleared, disable the request instead of submitting","Trim scope inputs client-side so accidental whitespace-only values are caught early"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const domain = req.domain?.trim().toLowerCase();\nconst country = req.country?.trim();\nif (!domain && !country) {\n  throw new ClientError('Timeline requires a domain or a country scope');\n}","typeGuard":"function isMissingScopeError(body: unknown): boolean {\n  return (body as { message?: string })?.message === 'At least one of domain or country is required';\n}","tryCatchPattern":"try {\n  await getIntelTimeline({ domain, country });\n} catch (e) {\n  if (e instanceof HttpError && e.status === 400 && isMissingScopeError(e.body)) {\n    return disableSubmit('Choose a domain or country filter');\n  }\n  throw e;\n}","preventionTips":["Disable timeline requests in the UI when both scope filters are cleared","Trim scope inputs client-side — whitespace-only values count as absent server-side too","Sending only from/to dates and limit never satisfies the scope requirement"],"tags":["intelligence","validation","missing-parameter","timeline"],"backgroundTag":"missing-required-argument","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}