{"record":{"id":"a590b9b6677238dd","repo":"koala73/worldmonitor","slug":"dynamic-apierror-400-message-via-local-invalid-helper-e-g","errorCode":null,"errorMessage":"Dynamic ApiError(400, message) via local invalid() helper (e.g. 'Date-search field is too long', 'Expected three-letter airport codes')","messagePattern":"Dynamic ApiError\\(400, message\\) via local invalid\\(\\) helper \\(e\\.g\\. 'Date-search field is too long', 'Expected three-letter airport codes'\\)","errorType":"validation","errorClass":"ApiError","httpStatus":400,"severity":"warning","filePath":"server/worldmonitor/aviation/v1/search-google-dates.ts","lineNumber":21,"sourceCode":"  SearchGoogleDatesRequest,\n  SearchGoogleDatesResponse,\n} from '../../../../src/generated/server/worldmonitor/aviation/v1/service_server';\nimport { ApiError } from '../../../../src/generated/server/worldmonitor/aviation/v1/service_server';\n// @ts-expect-error — JS module, no declaration file\nimport { sha256Hex } from '../../../../api/_crypto.js';\nimport { getRelayBaseUrl, getRelayHeaders } from '../../../_shared/relay';\nimport { parseStringArray } from '../../../_shared/parse-string-array';\nimport { normalizePassengerCount } from '../../../_shared/passenger-count';\nimport { cachedFetchJsonWithMeta } from '../../../_shared/redis';\n\n// Medium-cache tier (10 min) — use cachedFetchJsonWithMeta for stampede protection.\nconst CACHE_TTL = 600;\n\nexport async function searchGoogleDates(\n  _ctx: ServerContext,\n  req: SearchGoogleDatesRequest,\n): Promise<SearchGoogleDatesResponse> {\n  const invalid = (message: string): never => { throw new ApiError(400, message, ''); };\n  const bounded = (value: string | undefined, max: number): string => {\n    if ((value?.length ?? 0) > max) invalid('Date-search field is too long');\n    return (value ?? '').trim();\n  };\n  const origin = bounded(req.origin, 16).toUpperCase();\n  const destination = bounded(req.destination, 16).toUpperCase();\n  if (!/^[A-Z]{3}$/.test(origin) || !/^[A-Z]{3}$/.test(destination)) invalid('Expected three-letter airport codes');\n  const parseDate = (value: string): number => {\n    if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) invalid('Expected YYYY-MM-DD dates');\n    const time = Date.parse(value + 'T00:00:00Z');\n    if (!Number.isFinite(time) || new Date(time).toISOString().slice(0, 10) !== value) invalid('Invalid calendar date');\n    return time;\n  };\n  const startDate = bounded(req.startDate, 10);\n  const endDate = bounded(req.endDate, 10);\n  const days = (parseDate(endDate) - parseDate(startDate)) / 86_400_000 + 1;\n  // The relay supports six chunks of at most 61 days each.\n  if (days < 1 || days > 366) invalid('Date range must contain 1 to 366 days');","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/aviation/v1/search-google-dates.ts#L3-L39","documentation":"searchGoogleDates validates every request string field before use. The local invalid() helper throws ApiError(400) when any field exceeds its length cap (default message 'Date-search field is too long') or when a downstream check rejects the value, e.g. non-three-letter airport codes. It is a server-side input guard, not a data failure.","triggerScenarios":"Calling search-google-dates with req.origin or req.destination longer than 16 characters, or with values that fail the airport-code validation ('Expected three-letter airport codes').","commonSituations":"Passing full airport names ('San Francisco International') instead of IATA codes; pasting city+country strings; user-supplied free-text search boxes wired directly into the API without trimming/length checks.","solutions":["Send trimmed three-letter IATA codes (e.g. 'SFO') in origin/destination","Truncate or validate string length to 16 characters client-side before calling","Wrap the call in error handling that surfaces ApiError.message to the user and lets them re-enter the query"],"exampleFix":"// before\nawait searchGoogleDates(ctx, { origin: 'San Francisco International', destination: 'JFK', date });\n// after\nawait searchGoogleDates(ctx, { origin: 'SFO', destination: 'JFK', date });","handlingStrategy":"validation","validationCode":"const bad = [req.origin, req.destination].some(v => !v || v.trim().length === 0 || v.trim().length > 16);\nif (bad) throw new Error('origin/destination must be non-empty and <= 16 chars (IATA codes)');","typeGuard":"const isIata = (v: unknown): v is string => typeof v === 'string' && /^[A-Za-z]{3}$/.test(v.trim());","tryCatchPattern":"try {\n  return await searchGoogleDates(ctx, req);\n} catch (e) {\n  if (e instanceof ApiError && e.status === 400) return { error: e.message, retry: true };\n  throw e;\n}","preventionTips":["Use an airport autocomplete that only emits IATA codes","Trim and uppercase inputs before the call","Cap all string fields at 16 characters in the client form","Show field-level errors instead of blanket failure messages"],"tags":["validation","input","aviation"],"backgroundTag":"invalid-argument-value","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}