paperclipai/paperclip · error
Email attachment unavailable
Error message
Email attachment unavailable
What it means
When fetching an email attachment from a remote URL, emailChannelService uses guardedRemoteHttpFetch (SSRF/permission-guarded) with a 25s timeout. If the response is not ok (non-2xx) or has no body, it throws 'Email attachment unavailable'.
Solutions
- Check the attachment URL in a browser/curl to confirm it serves the file
- Re-request or re-upload the attachment to get a fresh signed URL
- Confirm the host is reachable from the Paperclip server (firewall/egress rules)
- If the URL requires auth, attach it via inline content instead of a link
Defensive patterns
Strategy: try-catch
Validate before calling
const head = await fetch(url, { method: "HEAD" });
if (!head.ok) throw new Error(`Attachment URL unreachable: HTTP ${head.status}`); Try / catch
try {
await emailAttachmentFetcher.fetch(url);
} catch (e) {
if (e instanceof Error && e.message === "Email attachment unavailable") {
notifySender("attachment link could not be downloaded");
} else throw e;
} Prevention
- Prefer durable attachment storage over expiring signed links
- Validate URLs return 2xx before relying on them downstream
- Avoid hosts that block datacenter IPs or require cookies for attachments
- Set a retry with backoff for transient 5xx before giving up
When it happens
Trigger: Attachment URL returns 404/403/500; the URL responds with an empty body or a response type without a streamable body; guardedRemoteHttpFetch blocks the URL (that path instead yields the badRequest 'Email attachment URL is not permitted').
Common situations: Sender links to a signed URL that expired; attachment host requires auth or cookies; CDN returns 403 to datacenter IPs; link rot on old emails.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- Anthropic Managed Agents request failed with HTTP
- Artifact download failed: HTTP
- Could not load OpenRouter models. Retry or enter a model ID…
- Discord returned an unreadable response
- OpenCode API request failed
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/640cf62868738d66.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/email-channels.ts:1010
omitted.push(attachment.filename ?? "attachment");
continue;
}
const locator = await api.getAttachment(
endpoint.botExternalId!,
message.message_id,
attachment.attachment_id,
);
const url = new URL(locator.download_url);
if (url.protocol !== "https:" || locator.size > MAX_ATTACHMENT_BYTES)
throw badRequest("Email attachment download is not permitted");
const { guardedRemoteHttpFetch } = await import("./remote-http-fetch.js");
const response = await guardedRemoteHttpFetch(
url,
{ signal: AbortSignal.timeout(25_000) },
{ error: () => badRequest("Email attachment URL is not permitted") },
);
if (!response.ok || !response.body)
throw new Error("Email attachment unavailable");
const reader = response.body.getReader();
const chunks: Buffer[] = [];
let size = 0;
try {
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
size += chunk.value.length;
if (size > MAX_ATTACHMENT_BYTES)
throw badRequest("Email attachment exceeds the size limit");
chunks.push(Buffer.from(chunk.value));
}
} finally {
await reader.cancel();
}
const stored = await options.storage.putFile({
companyId: endpoint.companyId,
namespace: `issues/${conversation.issueId}`,View on GitHub (pinned to 3f1d897a7c)