actualbudget/actual · error

Unable to resolve host: ${hostname}

Error message

Unable to resolve host: ${hostname}

What it means

When the URL host is not a literal IP, assertUrlAllowed resolves it via DNS (dnsLookup with { all: true }). If the lookup fails (NXDOMAIN, resolver outage, no network), it throws 'Unable to resolve host' naming the hostname, so the server never attempts the outbound request.

Source

Thrown at packages/sync-server/src/util/ssrf.ts:104

  // URL keeps the surrounding brackets on IPv6 hosts (e.g. "[::1]"); strip
  // them so the address can be parsed and resolved.
  const hostname = url.hostname.replace(/^\[|\]$/g, '');

  // Literal IP address: check it directly without a DNS lookup.
  if (ipaddr.isValid(hostname)) {
    if (isBlockedIp(hostname, options)) {
      throw new Error(`Blocked request to private/local IP: ${hostname}`);
    }
    return;
  }

  // Hostname: resolve every address it points to and reject if any is blocked.
  let addresses: { address: string }[];
  try {
    addresses = await dnsLookup(hostname, { all: true });
  } catch {
    throw new Error(`Unable to resolve host: ${hostname}`);
  }

  if (addresses.length === 0) {
    throw new Error(`Unable to resolve host: ${hostname}`);
  }

  for (const { address } of addresses) {
    if (isBlockedIp(address, options)) {
      throw new Error(
        `Blocked request to host resolving to private/local IP: ${hostname} (${address})`,
      );
    }
  }
}

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Verify the hostname is spelled correctly and exists in DNS (`nslookup <host>` from the server host).
  2. Register the internal hostname in a resolver the server can query, or use an IP-backed public name.
  3. Check the server's DNS configuration (/etc/resolv.conf, container DNS settings).
  4. Confirm the server host has outbound network access.

Example fix

// before
const url = 'https://sync.mydomain.con'; // typo
// after
const url = 'https://sync.mydomain.com'; // resolvable hostname
Defensive patterns

Strategy: retry

Validate before calling

import { lookup } from 'dns/promises';
async function hostResolves(hostname) {
  try {
    return (await lookup(hostname, { all: true })).length > 0;
  } catch {
    return false;
  }
}

Try / catch

try {
  await assertUrlAllowed(targetUrl);
} catch (e) {
  if (e.message.startsWith('Unable to resolve host')) {
    // transient resolver failures are common — retry with backoff
    await sleep(1000);
    return retryAssert(targetUrl, 3);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling claimAccessKey/getAccounts with a hostname that does not exist in DNS, a typo'd domain, a DNS resolver outage, or an internal-only hostname (e.g. 'sync.internal') that the server's resolver cannot answer.

Common situations: Typo in the bootstrap/server URL (e.g. '.con' instead of '.com'); self-hosted DNS names only resolvable on the LAN while the server runs elsewhere; DNS outage at the hosting provider; container without DNS configured.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/e441e6bec0aaeb98. Report an issue: GitHub.