{"record":{"id":"16e975c527b94c28","repo":"koala73/worldmonitor","slug":"dynamic-apierror-400-message-via-local-invalid-helper-e-g-16e975","errorCode":null,"errorMessage":"Dynamic ApiError(400, message) via local invalid() helper (e.g. 'Flight-search field is too long', 'Expected three-letter airport codes')","messagePattern":"Dynamic ApiError\\(400, message\\) via local invalid\\(\\) helper \\(e\\.g\\. 'Flight-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-flights.ts","lineNumber":21,"sourceCode":"  SearchGoogleFlightsRequest,\n  SearchGoogleFlightsResponse,\n} from '../../../../src/generated/server/worldmonitor/aviation/v1/service_server';\nimport { ApiError } from '../../../../src/generated/server/worldmonitor/aviation/v1/service_server';\nimport { IATA_RE } from './_shared';\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 { cachedFetchJson } from '../../../_shared/redis';\n\nconst CACHE_TTL = 600;\n\nexport async function searchGoogleFlights(\n  _ctx: ServerContext,\n  req: SearchGoogleFlightsRequest,\n): Promise<SearchGoogleFlightsResponse> {\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('Flight-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 (!IATA_RE.test(origin) || !IATA_RE.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 departureDate = bounded(req.departureDate, 10);\n  const returnDate = bounded(req.returnDate, 10);\n  const departureTime = parseDate(departureDate);\n  if (returnDate && parseDate(returnDate) < departureTime) invalid('Return date must not precede departure date');\n  const cabinClass = bounded(req.cabinClass, 32).toUpperCase() || 'ECONOMY';","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/aviation/v1/search-google-flights.ts#L3-L39","documentation":"searchGoogleFlights uses the same local invalid() helper as the dates endpoint: any request string field longer than its cap throws ApiError(400, 'Flight-search field is too long'), and format checks can throw 'Expected three-letter airport codes'. It rejects malformed input before any upstream Google Flights call.","triggerScenarios":"Calling search-google-flights with req.origin/req.destination over 16 characters, or non-IATA values failing the three-letter airport check; overly long optional fields also routed through bounded().","commonSituations":"Autocomplete fallback text submitted instead of a selected airport code; whitespace-padded or concatenated strings from form fields; UI allowing free-form origin/destination input.","solutions":["Trim and send 3-letter IATA codes for origin/destination","Enforce a 16-character max on all string fields before the call","Catch ApiError, show its message, and prompt the user to correct the airport fields"],"exampleFix":"// before\nawait searchGoogleFlights(ctx, { origin: userInput.origin, destination: 'LHR' });\n// after\nconst origin = userInput.origin.trim().toUpperCase().slice(0, 3);\nawait searchGoogleFlights(ctx, { origin, destination: 'LHR' });","handlingStrategy":"validation","validationCode":"const ok = [req.origin, req.destination].every(v => typeof v === 'string' && /^[A-Za-z]{3}$/.test(v.trim()));\nif (!ok) throw new Error('origin and destination must be three-letter IATA codes');","typeGuard":"const isShortCode = (v: unknown): v is string => typeof v === 'string' && v.trim().length <= 16 && v.trim().length > 0;","tryCatchPattern":"try {\n  return await searchGoogleFlights(ctx, req);\n} catch (e) {\n  if (e instanceof ApiError && e.status === 400) {\n    console.warn('flight search rejected:', e.message);\n    return null;\n  }\n  throw e;\n}","preventionTips":["Bind search inputs to a coded airport list, never free text","Normalize (trim/uppercase) before sending","Mirror the server's 16-char bound in your form validation"],"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"}