{"record":{"id":"a6e08e0e1c1f6f0f","repo":"different-ai/openwork","slug":"attachment-fetch-failed","errorCode":"attachment_fetch_failed","errorMessage":"Attachment url hostname could not be resolved","messagePattern":"Attachment url hostname could not be resolved","errorType":"http","errorClass":"ApiError","httpStatus":502,"severity":"error","filePath":"apps/server/src/extensions/google-workspace.ts","lineNumber":831,"sourceCode":"  const privateHost = hostname === \"localhost\" || hostname.endsWith(\".localhost\") || hostname.endsWith(\".local\") || (isIP(hostname) !== 0 && isLocalManagedMcpPrivateAddress(hostname));\n  if (parsed.protocol !== \"https:\" || parsed.username || parsed.password || privateHost) {\n    throw new ApiError(400, \"invalid_payload\", \"Attachment url must be a public https URL\", { url: rawUrl });\n  }\n  return parsed;\n}\n\n// Hostnames must resolve to public addresses, unconditionally, like the\n// literal-address checks above. externalFetch re-resolves when it connects,\n// so this validates every answer we can observe but cannot pin the socket to\n// it the way the managed MCP undici dispatcher does.\nasync function assertGmailAttachmentHostPublic(url: URL): Promise<void> {\n  const hostname = url.hostname.replace(/^\\[|\\]$/g, \"\");\n  if (isIP(hostname) !== 0) return;\n  let addresses: LookupAddress[];\n  try {\n    addresses = await lookup(hostname, { all: true, verbatim: true });\n  } catch {\n    throw new ApiError(502, \"attachment_fetch_failed\", \"Attachment url hostname could not be resolved\", { hostname });\n  }\n  if (!addresses.length || addresses.some((entry) => isLocalManagedMcpPrivateAddress(entry.address))) {\n    throw new ApiError(400, \"invalid_payload\", \"Attachment url must resolve to a public address\", { hostname });\n  }\n}\n\nconst resolveGmailAttachmentAddresses: GmailAttachmentAddressResolver = (hostname, options) => lookup(hostname, options);\n\nfunction validateGmailAttachmentAddresses(hostname: string, addresses: LookupAddress[]): void {\n  if (!addresses.length) throw new Error(`Attachment hostname ${hostname} did not resolve.`);\n  const privateAddress = addresses.find((entry) => isLocalManagedMcpPrivateAddress(entry.address));\n  if (privateAddress) {\n    throw new Error(`Attachment hostname ${hostname} resolved to a private or reserved address (${privateAddress.address}).`);\n  }\n}\n\n/**\n * Resolves and validates the attachment host inside the socket connector. The","sourceCodeStart":813,"sourceCodeEnd":849,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/extensions/google-workspace.ts#L813-L849","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n{ url: \"https://attachments.old-domain.example/file.pdf\" } // NXDOMAIN\n// after\n{ url: \"https://attachments.example.com/file.pdf\" } // resolves publicly","handlingStrategy":"retry","validationCode":"import { lookup } from \"node:dns/promises\";\nasync function hostnameResolves(hostname: string): Promise<boolean> {\n  try { return (await lookup(hostname, { all: true })).length > 0; } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await fetchGmailAttachment({ url });\n} catch (e) {\n  if (e instanceof ApiError && e.code === \"attachment_fetch_failed\" && /could not be resolved/.test(e.message)) {\n    await sleep(1000);\n    return fetchGmailAttachment({ url }); // retry once after transient DNS failure\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["dns","network","gmail","attachments"],"backgroundTag":"dns-resolution-failed","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}