different-ai/openwork · error · ApiError

attachment_fetch_failed

attachment_fetch_failed

Error message

Attachment url hostname could not be resolved

What it means

assertGmailAttachmentHostPublic performs a DNS lookup (all addresses, verbatim) for non-IP-literal hostnames before fetching. If lookup() throws (resolver failure, NXDOMAIN surfaces as ENOTFOUND here), the server throws 502 attachment_fetch_failed because it cannot verify the host is public. If it resolves but any address is private/reserved, a separate invalid_payload error is thrown instead.

Source

Thrown at apps/server/src/extensions/google-workspace.ts:831

  const privateHost = hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local") || (isIP(hostname) !== 0 && isLocalManagedMcpPrivateAddress(hostname));
  if (parsed.protocol !== "https:" || parsed.username || parsed.password || privateHost) {
    throw new ApiError(400, "invalid_payload", "Attachment url must be a public https URL", { url: rawUrl });
  }
  return parsed;
}

// Hostnames must resolve to public addresses, unconditionally, like the
// literal-address checks above. externalFetch re-resolves when it connects,
// so this validates every answer we can observe but cannot pin the socket to
// it the way the managed MCP undici dispatcher does.
async function assertGmailAttachmentHostPublic(url: URL): Promise<void> {
  const hostname = url.hostname.replace(/^\[|\]$/g, "");
  if (isIP(hostname) !== 0) return;
  let addresses: LookupAddress[];
  try {
    addresses = await lookup(hostname, { all: true, verbatim: true });
  } catch {
    throw new ApiError(502, "attachment_fetch_failed", "Attachment url hostname could not be resolved", { hostname });
  }
  if (!addresses.length || addresses.some((entry) => isLocalManagedMcpPrivateAddress(entry.address))) {
    throw new ApiError(400, "invalid_payload", "Attachment url must resolve to a public address", { hostname });
  }
}

const resolveGmailAttachmentAddresses: GmailAttachmentAddressResolver = (hostname, options) => lookup(hostname, options);

function validateGmailAttachmentAddresses(hostname: string, addresses: LookupAddress[]): void {
  if (!addresses.length) throw new Error(`Attachment hostname ${hostname} did not resolve.`);
  const privateAddress = addresses.find((entry) => isLocalManagedMcpPrivateAddress(entry.address));
  if (privateAddress) {
    throw new Error(`Attachment hostname ${hostname} resolved to a private or reserved address (${privateAddress.address}).`);
  }
}

/**
 * Resolves and validates the attachment host inside the socket connector. The

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Verify the hostname resolves publicly (nslookup/dig the hostname) and fix the URL if the domain is wrong.
  2. Fix local DNS: check resolv.conf/VPN and retry; consider pointing at a working resolver.
  3. If the record was just created, wait for DNS propagation and retry.
  4. If the host must be internal, do not use this code path — fetch/proxy the attachment through an approved external service.

Example fix

// before
{ url: "https://attachments.old-domain.example/file.pdf" } // NXDOMAIN
// after
{ url: "https://attachments.example.com/file.pdf" } // resolves publicly
Defensive patterns

Strategy: retry

Validate before calling

import { lookup } from "node:dns/promises";
async function hostnameResolves(hostname: string): Promise<boolean> {
  try { return (await lookup(hostname, { all: true })).length > 0; } catch { return false; }
}

Try / catch

try {
  await fetchGmailAttachment({ url });
} catch (e) {
  if (e instanceof ApiError && e.code === "attachment_fetch_failed" && /could not be resolved/.test(e.message)) {
    await sleep(1000);
    return fetchGmailAttachment({ url }); // retry once after transient DNS failure
  }
  throw e;
}

Prevention

When it happens

Trigger: Fetching an attachment whose hostname fails DNS resolution: typo'd domain, expired domain, DNS server unreachable, IPv6-only lookup failing, or NXDOMAIN for a deleted host.

Common situations: Temporary corporate DNS/VPN outage; hostname only resolvable on an internal DNS zone (which would then also fail the public check); stale URLs from old emails whose hosts no longer exist; resolv.conf misconfiguration in containers.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/a6e08e0e1c1f6f0f. Report an issue: GitHub.