koala73/worldmonitor · error

UNTRUSTED_SOURCE_HOST

Error message

UNTRUSTED_SOURCE_HOST

What it means

fetchApprovedImdJson() validates every URL against isAllowedImdHost() before fetching: the URL must be https, exactly the allowed IMD hostname, on port 443 (or default), with no embedded credentials. Any URL failing this SSRF guard throws UNTRUSTED_SOURCE_HOST without making a network request. It is an intentional security control, not a network failure.

Solutions

  1. Use imdProductUrl(product) to build the URL so it always targets the approved IMD host over https.
  2. If you must fetch from another host, host your data or copy it to the approved host; do not bypass the check.
  3. Verify the URL parses as https with the exact allowed hostname, port 443/default, and no credentials: run isAllowedImdHost(url) in a REPL.
  4. Fix typos or stray query components/credentials in the configured URL.

Example fix

// before
await fetchApprovedImdJson('http://maaalaimaatham.gov.in/data.json');
// after
import { imdProductUrl } from './imd-cyclone-marine.mjs';
await fetchApprovedImdJson(imdProductUrl(product));
Defensive patterns

Strategy: validation

Validate before calling

import { isAllowedImdHost, imdProductUrl } from './imd-cyclone-marine.mjs';
const url = imdProductUrl(product);
if (!isAllowedImdHost(url)) {
  throw new Error(`Refusing to fetch non-approved IMD URL: ${url}`);
}
await fetchApprovedImdJson(url);

Type guard

function isSafeImdUrl(url) {
  try {
    const u = new URL(String(url));
    return u.protocol === 'https:'
      && u.hostname.toLowerCase() === 'maaalaimaatham' // exact allowed IMD host
      && (u.port === '' || u.port === '443')
      && u.username === '' && u.password === '';
  } catch {
    return false;
  }
}

Try / catch

try {
  const data = await fetchApprovedImdJson(url);
} catch (err) {
  if (err.message === 'UNTRUSTED_SOURCE_HOST') {
    console.error(`Blocked non-approved IMD host: ${url}`);
    return fallbackSnapshot;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling fetchApprovedImdJson('http://maaalaimaatham/path') (non-https), a mirror/lookalike hostname, a URL with a custom port, a URL with user:pass@ credentials, or a non-URL string.

Common situations: Hard-coding an http:// URL during local testing; pointing the fetcher at a staging/mock host or a local proxy URL; data-driven URLs built from user or upstream input that are not on the approved host; typos in the hostname.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


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

Appendix: source

Thrown at scripts/lib/imd-cyclone-marine.mjs:755

    const { done, value } = await reader.read();
    if (done) break;
    size += value.byteLength;
    if (size > maxBytes) throw new Error(`IMD_RESPONSE_TOO_LARGE:${size}`);
    chunks.push(value);
  }
  return JSON.parse(new TextDecoder().decode(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)))));
}

export async function fetchApprovedImdJson(url, {
  fetchFn = globalThis.fetch,
  userAgent = CHROME_UA,
  maxBytes = IMD_MAX_BYTES,
  timeoutMs = IMD_TIMEOUT_MS,
  apiKey = null,
  apiKeyHeader = 'X-API-Key',
  apiToken = null,
} = {}) {
  if (!isAllowedImdHost(url)) throw new Error('UNTRUSTED_SOURCE_HOST');
  const headers = { Accept: 'application/json', 'User-Agent': userAgent };
  if (apiKey) headers[apiKeyHeader] = apiKey;
  if (apiToken) headers.Authorization = `Bearer ${apiToken}`;
  const response = await fetchFn(url, {
    headers,
    redirect: 'error',
    signal: AbortSignal.timeout(timeoutMs),
  });
  if (!response.ok) {
    const err = new Error(`HTTP ${response.status}`);
    err.httpStatus = response.status;
    throw err;
  }
  return readBoundedJsonResponse(response, maxBytes);
}

export function createImdProxyFetch(rawProxyUrl, { proxyFetchFn = proxyFetch } = {}) {
  const proxyUrl = String(rawProxyUrl || '').trim();

View on GitHub (pinned to 7d06c8633d)