{"record":{"id":"e441e6bec0aaeb98","repo":"actualbudget/actual","slug":"unable-to-resolve-host-hostname","errorCode":null,"errorMessage":"Unable to resolve host: ${hostname}","messagePattern":"Unable to resolve host: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/sync-server/src/util/ssrf.ts","lineNumber":104,"sourceCode":"\n  // URL keeps the surrounding brackets on IPv6 hosts (e.g. \"[::1]\"); strip\n  // them so the address can be parsed and resolved.\n  const hostname = url.hostname.replace(/^\\[|\\]$/g, '');\n\n  // Literal IP address: check it directly without a DNS lookup.\n  if (ipaddr.isValid(hostname)) {\n    if (isBlockedIp(hostname, options)) {\n      throw new Error(`Blocked request to private/local IP: ${hostname}`);\n    }\n    return;\n  }\n\n  // Hostname: resolve every address it points to and reject if any is blocked.\n  let addresses: { address: string }[];\n  try {\n    addresses = await dnsLookup(hostname, { all: true });\n  } catch {\n    throw new Error(`Unable to resolve host: ${hostname}`);\n  }\n\n  if (addresses.length === 0) {\n    throw new Error(`Unable to resolve host: ${hostname}`);\n  }\n\n  for (const { address } of addresses) {\n    if (isBlockedIp(address, options)) {\n      throw new Error(\n        `Blocked request to host resolving to private/local IP: ${hostname} (${address})`,\n      );\n    }\n  }\n}\n","sourceCodeStart":86,"sourceCodeEnd":119,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/util/ssrf.ts#L86-L119","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify the hostname is spelled correctly and exists in DNS (`nslookup <host>` from the server host).","Register the internal hostname in a resolver the server can query, or use an IP-backed public name.","Check the server's DNS configuration (/etc/resolv.conf, container DNS settings).","Confirm the server host has outbound network access."],"exampleFix":"// before\nconst url = 'https://sync.mydomain.con'; // typo\n// after\nconst url = 'https://sync.mydomain.com'; // resolvable hostname","handlingStrategy":"retry","validationCode":"import { lookup } from 'dns/promises';\nasync function hostResolves(hostname) {\n  try {\n    return (await lookup(hostname, { all: true })).length > 0;\n  } catch {\n    return false;\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await assertUrlAllowed(targetUrl);\n} catch (e) {\n  if (e.message.startsWith('Unable to resolve host')) {\n    // transient resolver failures are common — retry with backoff\n    await sleep(1000);\n    return retryAssert(targetUrl, 3);\n  }\n  throw e;\n}","preventionTips":["Verify hostnames in DNS before configuring them (`dig <host> +short`).","Ensure the server host/container has working DNS resolvers.","Prefer stable public DNS records for endpoints.","Retry DNS-dependent checks a few times before failing."],"tags":["dns","network","ssrf","sync-server"],"backgroundTag":"dns-resolution-failed","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}