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. 'Date-search field is too long', 'Expected three-letter airport codes')
What it means
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.
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
Example fix
// before
await searchGoogleDates(ctx, { origin: 'San Francisco International', destination: 'JFK', date });
// after
await searchGoogleDates(ctx, { origin: 'SFO', destination: 'JFK', date }); Defensive patterns
Strategy: validation
Validate before calling
const bad = [req.origin, req.destination].some(v => !v || v.trim().length === 0 || v.trim().length > 16);
if (bad) throw new Error('origin/destination must be non-empty and <= 16 chars (IATA codes)'); Type guard
const isIata = (v: unknown): v is string => typeof v === 'string' && /^[A-Za-z]{3}$/.test(v.trim()); Try / catch
try {
return await searchGoogleDates(ctx, req);
} catch (e) {
if (e instanceof ApiError && e.status === 400) return { error: e.message, retry: true };
throw e;
} Prevention
- 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
When it happens
Trigger: 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').
Common situations: 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.
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
- Dynamic ApiError(400, message) via local invalid() helper (e
- Could not resolve ${JSON.stringify(echoCountryInput(raw))} t
- provide exactly one of country_code, preset, or members.
- Aircraft identifier is too long
- Expected a six-character hexadecimal ICAO address
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/a590b9b6677238dd.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/aviation/v1/search-google-dates.ts:21
SearchGoogleDatesRequest,
SearchGoogleDatesResponse,
} from '../../../../src/generated/server/worldmonitor/aviation/v1/service_server';
import { ApiError } from '../../../../src/generated/server/worldmonitor/aviation/v1/service_server';
// @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 { cachedFetchJsonWithMeta } from '../../../_shared/redis';
// Medium-cache tier (10 min) — use cachedFetchJsonWithMeta for stampede protection.
const CACHE_TTL = 600;
export async function searchGoogleDates(
_ctx: ServerContext,
req: SearchGoogleDatesRequest,
): Promise<SearchGoogleDatesResponse> {
const invalid = (message: string): never => { throw new ApiError(400, message, ''); };
const bounded = (value: string | undefined, max: number): string => {
if ((value?.length ?? 0) > max) invalid('Date-search field is too long');
return (value ?? '').trim();
};
const origin = bounded(req.origin, 16).toUpperCase();
const destination = bounded(req.destination, 16).toUpperCase();
if (!/^[A-Z]{3}$/.test(origin) || !/^[A-Z]{3}$/.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 startDate = bounded(req.startDate, 10);
const endDate = bounded(req.endDate, 10);
const days = (parseDate(endDate) - parseDate(startDate)) / 86_400_000 + 1;
// The relay supports six chunks of at most 61 days each.
if (days < 1 || days > 366) invalid('Date range must contain 1 to 366 days');View on GitHub (pinned to 7d06c8633d)