can1357/oh-my-pi · error

Discord response did not include an attachment

Error message

Discord response did not include an attachment

What it means

Thrown by parseMessage() when the Discord webhook response contains a valid message `id` but `attachments` is missing or is not an array. The uploader uploads a file via multipart and requires the resulting message to carry at least one attachment object, from which it extracts the CDN URL for the published blob. Its absence means Discord did not process the file as an attachment.

Source

Thrown at packages/coding-agent/src/blob-broker/uploaders-discord.ts:57

	const webhooksIndex = segments.indexOf("webhooks");
	const id = webhooksIndex >= 0 ? segments[webhooksIndex + 1] : undefined;
	const token = webhooksIndex >= 0 ? segments[webhooksIndex + 2] : undefined;
	if (!id || !token || !/^\d+$/.test(id)) {
		throw new Error("Discord webhook credential does not contain a webhook ID and token");
	}
	return { id, token };
}

function webhookEndpoint(webhook: DiscordWebhook, suffix?: string): URL {
	const base = `${DISCORD_API_ORIGIN}/api/v10/webhooks/${encodeURIComponent(webhook.id)}/${encodeURIComponent(webhook.token)}`;
	return new URL(suffix ? `${base}/${suffix}` : base);
}

function parseMessage(value: unknown): DiscordMessage {
	if (!value || typeof value !== "object") throw new Error("Discord returned an invalid message response");
	const message = value as Record<string, unknown>;
	if (typeof message.id !== "string") throw new Error("Discord response did not include a message ID");
	if (!Array.isArray(message.attachments)) throw new Error("Discord response did not include an attachment");
	const first = message.attachments[0];
	if (!first || typeof first !== "object") throw new Error("Discord response did not include an attachment");
	const attachmentUrl = (first as Record<string, unknown>).url;
	if (typeof attachmentUrl !== "string") throw new Error("Discord attachment did not include a URL");
	try {
		const parsed = new URL(attachmentUrl);
		if (parsed.protocol !== "https:") throw new Error();
	} catch {
		throw new Error("Discord attachment URL is invalid");
	}
	return { id: message.id, attachmentUrl };
}

function attachmentExpiry(url: string, now: number): number {
	const signedExpiry = new URL(url).searchParams.get("ex");
	if (signedExpiry && /^[0-9a-f]+$/i.test(signedExpiry)) {
		const seconds = Number.parseInt(signedExpiry, 16);
		const expiresAt = seconds * 1_000;

View on GitHub (pinned to 9690622007)

Solutions

  1. Verify payload_json attachments[{id:0, filename}] matches the multipart field name files[0] and the declared filename exactly.
  2. Log the response body to inspect what Discord returned instead of an attachments array.
  3. Check the file size/content-type against Discord's webhook upload limit (8–25MB depending on tier).
  4. Retry the upload — transient Discord issues can drop attachments.
  5. Test the webhook manually with curl using the same multipart payload to isolate the uploader.
Defensive patterns

Strategy: validation

Validate before calling

function hasAttachments(body: unknown): boolean {
	return !!body && typeof body === "object" && Array.isArray((body as Record<string, unknown>).attachments);
}
// call: if (!hasAttachments(await response.clone().json())) throw new Error("discord dropped attachment");

Type guard

function hasAttachmentArray(v: unknown): v is { attachments: Record<string, unknown>[] } {
	return !!v && typeof v === "object" && Array.isArray((v as Record<string, unknown>).attachments);
}

Try / catch

try {
	const message = parseMessage(await response.json());
} catch (err) {
	if (err instanceof Error && err.message.includes("attachment")) {
		// inspect payload_json/files[0] pairing, then retry once
	}
	throw err;
}

Prevention

When it happens

Trigger: Webhook POST with files[0] multipart body returned a message object without an attachments array — e.g. Discord accepted the message but dropped the file, payload_json mismatched the files[] indices, a proxy downgraded the request, or a thread_id pointed somewhere attachments were not returned.

Common situations: payload_json attachment metadata (id/filename) mismatched with files[0], causing Discord to discard the file; upload size rejected but message still posted (text-only content); API behavior changes in newer Discord webhook versions; intermediaries stripping multipart file parts.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/63a7207d0d9e8fad. Report an issue: GitHub.