koala73/worldmonitor · error · ValidationError
hs4 must be a supported four-digit heading
Error message
hs4 must be a supported four-digit heading
What it means
getCountryProducts accepts an optional `hs4` filter restricted to a fixed allowlist of supported four-digit HS headings (HS4_CODES). If hs4 is provided but is not in that list, a ValidationError with field 'hs4' is thrown; note the endpoint also returns an empty product list for non-premium callers.
Solutions
- Check hs4 against the supported HS4_CODES list (fetch the unfiltered products response to see which headings are returned) and pick a supported one
- Normalize input: trim and pass exactly four characters, digits only (e.g. '8501', not '8501.10' or ' 8501 ')
- Omit hs4 entirely to get all tracked products for the country, then filter client-side
- If you need an untracked heading, request it be added to HS4_CODES rather than sending it to the endpoint
Example fix
// before
await client.getCountryProducts({ iso2: 'DE', hs4: '8501.10' });
// after
const hs4 = req.hs4?.trim();
if (hs4 && !/^\d{4}$/.test(hs4)) throw new Error('hs4 must be 4 digits');
await client.getCountryProducts({ iso2: 'DE', hs4 }); // or omit hs4 and filter locally Defensive patterns
Strategy: validation
Validate before calling
const SUPPORTED_HS4 = ['8501','8471','5201']; // mirror of HS4_CODES / from API response
if (hs4 !== undefined && hs4 !== '' && !SUPPORTED_HS4.includes(hs4.trim())) {
throw new Error(`Unsupported hs4 heading: ${hs4}`);
} Type guard
function isSupportedHs4(v: string): boolean {
return /^\d{4}$/.test(v) && HS4_CODES.includes(v);
} Try / catch
try {
return await client.getCountryProducts({ iso2, hs4 });
} catch (e) {
if (e instanceof ValidationError && e.fields?.[0]?.field === 'hs4') {
return client.getCountryProducts({ iso2 }); // all tracked products, filter client-side
}
throw e;
} Prevention
- Trim and normalize hs4 to exactly four digits before sending
- Do not confuse HS chapters (2-digit) or 6-digit lines with 4-digit headings
- Build hs4 pickers from the endpoint's own supported list, not raw tariff tables
When it happens
Trigger: Calling get-country-products with hs4 = '12345' (5 digits), '85' (2-digit chapter), a lowercase or padded value, or a valid-looking HS4 code that the platform simply does not track.
Common situations: Confusing HS chapters (2-digit) or lines (6+ digit) with 4-digit headings; using a code valid in the full HS taxonomy but absent from the tracked subset; trailing spaces from spreadsheet data; non-premium callers probing the parameter expecting data.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- Unsupported arXiv category
- Invalid scorecard bloc selection.
- Expected an array of Sentry issues. A Sentry error body is a
- Sentry issues response was not an array
- Redis transaction failed: ${results?.error || 'invalid respo
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/3182eb01a0f10fbf.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/supply-chain/v1/get-country-products.ts:209
...product,
topExporters: product.topExporters.map(exporter => {
const scale = byCode.get(exporter.partnerCode);
return scale && scale.year === product.year ? { ...exporter, scale } : exporter;
}),
};
}
export async function getCountryProducts(
ctx: ServerContext,
req: GetCountryProductsRequest,
): Promise<GetCountryProductsResponse> {
const iso2 = (req.iso2 ?? '').trim().toUpperCase();
if (!/^[A-Z]{2}$/.test(iso2)) {
throw new ValidationError([{ field: 'iso2', description: 'iso2 must be a 2-letter uppercase ISO country code' }]);
}
const hs4 = req.hs4?.trim();
if (hs4 && !HS4_CODES.includes(hs4)) {
throw new ValidationError([{ field: 'hs4', description: 'hs4 must be a supported four-digit heading' }]);
}
const isPro = await isCallerPremium(ctx.request);
const empty: GetCountryProductsResponse = { iso2, products: [], fetchedAt: '' };
if (!isPro) return empty;
const key = `comtrade:bilateral-hs4:${iso2}:v1`;
// Status-aware reads for the two country keys so a read error stays
// distinguishable from a miss; the canonical one decides cache_unavailable,
// the sibling one only decides how deep the origins go.
// The sibling detail and the world-exports snapshot are served only for a
// requested heading, so a whole-catalogue caller (the deep-dive panel) does
// not read them: the snapshot is a few hundred kilobytes (36 headings x ~140
// reporters), past what the 1.5 s single-GET deadline is sized for, and the
// large-value reader waits on the pipeline deadline instead.
const [cached, siblingRead, worldExportsValue, meta] = await Promise.all([
readCachedJson(key, true),
hs4 ? readCachedJson(PARTNERS_KEY(iso2), true) : { status: 'miss' as const },
hs4 ? getLargeRawJson(WORLD_EXPORTS_KEY).catch(() => null) : null,View on GitHub (pinned to 7d06c8633d)