actualbudget/actual · error

Unable to resolve host

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.

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.

Appendix: 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)