koala73/worldmonitor · warning · ApiError

Dynamic ApiError(400, message) via local invalid() helper (e

Error message

Dynamic ApiError(400, message) via local invalid() helper (e.g. 'Flight-search field is too long', 'Expected three-letter airport codes')

What it means

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.

Solutions

  1. Trim and send 3-letter IATA codes for origin/destination
  2. Enforce a 16-character max on all string fields before the call
  3. Catch ApiError, show its message, and prompt the user to correct the airport fields

Example fix

// before
await searchGoogleFlights(ctx, { origin: userInput.origin, destination: 'LHR' });
// after
const origin = userInput.origin.trim().toUpperCase().slice(0, 3);
await searchGoogleFlights(ctx, { origin, destination: 'LHR' });
Defensive patterns

Strategy: validation

Validate before calling

const ok = [req.origin, req.destination].every(v => typeof v === 'string' && /^[A-Za-z]{3}$/.test(v.trim()));
if (!ok) throw new Error('origin and destination must be three-letter IATA codes');

Type guard

const isShortCode = (v: unknown): v is string => typeof v === 'string' && v.trim().length <= 16 && v.trim().length > 0;

Try / catch

try {
  return await searchGoogleFlights(ctx, req);
} catch (e) {
  if (e instanceof ApiError && e.status === 400) {
    console.warn('flight search rejected:', e.message);
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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().

Common situations: 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.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/16e975c527b94c28. Report an issue: GitHub.

Appendix: source

Thrown at server/worldmonitor/aviation/v1/search-google-flights.ts:21

  SearchGoogleFlightsRequest,
  SearchGoogleFlightsResponse,
} from '../../../../src/generated/server/worldmonitor/aviation/v1/service_server';
import { ApiError } from '../../../../src/generated/server/worldmonitor/aviation/v1/service_server';
import { IATA_RE } from './_shared';
// @ts-expect-error — JS module, no declaration file
import { sha256Hex } from '../../../../api/_crypto.js';
import { getRelayBaseUrl, getRelayHeaders } from '../../../_shared/relay';
import { parseStringArray } from '../../../_shared/parse-string-array';
import { normalizePassengerCount } from '../../../_shared/passenger-count';
import { cachedFetchJson } from '../../../_shared/redis';

const CACHE_TTL = 600;

export async function searchGoogleFlights(
  _ctx: ServerContext,
  req: SearchGoogleFlightsRequest,
): Promise<SearchGoogleFlightsResponse> {
  const invalid = (message: string): never => { throw new ApiError(400, message, ''); };
  const bounded = (value: string | undefined, max: number): string => {
    if ((value?.length ?? 0) > max) invalid('Flight-search field is too long');
    return (value ?? '').trim();
  };
  const origin = bounded(req.origin, 16).toUpperCase();
  const destination = bounded(req.destination, 16).toUpperCase();
  if (!IATA_RE.test(origin) || !IATA_RE.test(destination)) invalid('Expected three-letter airport codes');
  const parseDate = (value: string): number => {
    if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) invalid('Expected YYYY-MM-DD dates');
    const time = Date.parse(value + 'T00:00:00Z');
    if (!Number.isFinite(time) || new Date(time).toISOString().slice(0, 10) !== value) invalid('Invalid calendar date');
    return time;
  };
  const departureDate = bounded(req.departureDate, 10);
  const returnDate = bounded(req.returnDate, 10);
  const departureTime = parseDate(departureDate);
  if (returnDate && parseDate(returnDate) < departureTime) invalid('Return date must not precede departure date');
  const cabinClass = bounded(req.cabinClass, 32).toUpperCase() || 'ECONOMY';

View on GitHub (pinned to 7d06c8633d)