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. TheView on GitHub (pinned to 2b7df46e8a)
Solutions
- Verify the hostname resolves publicly (nslookup/dig the hostname) and fix the URL if the domain is wrong.
- Fix local DNS: check resolv.conf/VPN and retry; consider pointing at a working resolver.
- If the record was just created, wait for DNS propagation and retry.
- 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
- Verify DNS resolution (dig/nslookup) before integrating a new attachment host.
- Watch for VPN/DNS outages in corporate environments.
- Avoid freshly created or about-to-expire domains for hosted attachments.
- Use stable, reputable DNS resolvers on the server.
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
- Attachment hostname ${hostname} did not resolve.
- uploadErrorMessage(metadata.filename, error)
- the hostname does not resolve
- latest-mac.yml is missing artifact path/url.
- Failed to fetch latest-mac.yml (${response.status} ${respons
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/a6e08e0e1c1f6f0f.
Report an issue: GitHub.