{"record":{"id":"63a7207d0d9e8fad","repo":"can1357/oh-my-pi","slug":"discord-response-did-not-include-an-attachment","errorCode":null,"errorMessage":"Discord response did not include an attachment","messagePattern":"Discord response did not include an attachment","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/uploaders-discord.ts","lineNumber":57,"sourceCode":"\tconst webhooksIndex = segments.indexOf(\"webhooks\");\n\tconst id = webhooksIndex >= 0 ? segments[webhooksIndex + 1] : undefined;\n\tconst token = webhooksIndex >= 0 ? segments[webhooksIndex + 2] : undefined;\n\tif (!id || !token || !/^\\d+$/.test(id)) {\n\t\tthrow new Error(\"Discord webhook credential does not contain a webhook ID and token\");\n\t}\n\treturn { id, token };\n}\n\nfunction webhookEndpoint(webhook: DiscordWebhook, suffix?: string): URL {\n\tconst base = `${DISCORD_API_ORIGIN}/api/v10/webhooks/${encodeURIComponent(webhook.id)}/${encodeURIComponent(webhook.token)}`;\n\treturn new URL(suffix ? `${base}/${suffix}` : base);\n}\n\nfunction parseMessage(value: unknown): DiscordMessage {\n\tif (!value || typeof value !== \"object\") throw new Error(\"Discord returned an invalid message response\");\n\tconst message = value as Record<string, unknown>;\n\tif (typeof message.id !== \"string\") throw new Error(\"Discord response did not include a message ID\");\n\tif (!Array.isArray(message.attachments)) throw new Error(\"Discord response did not include an attachment\");\n\tconst first = message.attachments[0];\n\tif (!first || typeof first !== \"object\") throw new Error(\"Discord response did not include an attachment\");\n\tconst attachmentUrl = (first as Record<string, unknown>).url;\n\tif (typeof attachmentUrl !== \"string\") throw new Error(\"Discord attachment did not include a URL\");\n\ttry {\n\t\tconst parsed = new URL(attachmentUrl);\n\t\tif (parsed.protocol !== \"https:\") throw new Error();\n\t} catch {\n\t\tthrow new Error(\"Discord attachment URL is invalid\");\n\t}\n\treturn { id: message.id, attachmentUrl };\n}\n\nfunction attachmentExpiry(url: string, now: number): number {\n\tconst signedExpiry = new URL(url).searchParams.get(\"ex\");\n\tif (signedExpiry && /^[0-9a-f]+$/i.test(signedExpiry)) {\n\t\tconst seconds = Number.parseInt(signedExpiry, 16);\n\t\tconst expiresAt = seconds * 1_000;","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/uploaders-discord.ts#L39-L75","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify payload_json attachments[{id:0, filename}] matches the multipart field name files[0] and the declared filename exactly.","Log the response body to inspect what Discord returned instead of an attachments array.","Check the file size/content-type against Discord's webhook upload limit (8–25MB depending on tier).","Retry the upload — transient Discord issues can drop attachments.","Test the webhook manually with curl using the same multipart payload to isolate the uploader."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"function hasAttachments(body: unknown): boolean {\n\treturn !!body && typeof body === \"object\" && Array.isArray((body as Record<string, unknown>).attachments);\n}\n// call: if (!hasAttachments(await response.clone().json())) throw new Error(\"discord dropped attachment\");","typeGuard":"function hasAttachmentArray(v: unknown): v is { attachments: Record<string, unknown>[] } {\n\treturn !!v && typeof v === \"object\" && Array.isArray((v as Record<string, unknown>).attachments);\n}","tryCatchPattern":"try {\n\tconst message = parseMessage(await response.json());\n} catch (err) {\n\tif (err instanceof Error && err.message.includes(\"attachment\")) {\n\t\t// inspect payload_json/files[0] pairing, then retry once\n\t}\n\tthrow err;\n}","preventionTips":["Keep payload_json attachments[{id:0,...}] index in sync with the files[0] multipart field name.","Check file size against Discord's webhook upload limit before uploading.","Never send a zero-byte or unreadable file stream.","Test webhook uploads manually with curl when configuring a new destination."],"tags":["discord","webhook","attachment","response-validation"],"backgroundTag":"missing-attachment-in-response","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}