{"record":{"id":"ebd366205705c113","repo":"can1357/oh-my-pi","slug":"discord-response-did-not-include-a-message-id","errorCode":null,"errorMessage":"Discord response did not include a message ID","messagePattern":"Discord response did not include a message ID","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/uploaders-discord.ts","lineNumber":56,"sourceCode":"\tconst segments = url.pathname.split(\"/\").filter(Boolean);\n\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);","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/uploaders-discord.ts#L38-L74","documentation":"This error is thrown by parseMessage() in the Discord webhook blob uploader when the JSON body returned by Discord's execute-webhook endpoint (called with ?wait=true) parses successfully but has no string `id` field. The uploader needs the message ID both as `remoteId` for the publication record and to build the DELETE URL used to later remove the message/blob. It indicates the response was not a normal Discord message object — typically an error payload, an empty body, or a non-wait response shape.","triggerScenarios":"POST to https://discord.com/api/v10/webhooks/<id>/<token>?wait=true succeeded at the HTTP layer (expectOk passed) but response.json() yielded an object whose `id` is missing or not a string — e.g. Discord returned {\"error\": ...}, an empty object, or the request hit a proxy returning a different JSON shape; also when `wait` was stripped by an intermediary so Discord returns 204/empty body serialized oddly.","commonSituations":"Corporate proxies or custom fetch wrappers rewriting Discord responses; Discord API incidents returning degraded payloads; misconfigured thread_id causing a different response shape; a webhook token pointing at an endpoint that returns JSON error bodies with a 200-level status via a gateway/proxy; stale Discord API version behavior differences (v10 without wait semantics).","solutions":["Confirm the webhook request URL includes ?wait=true (the uploader sets this; verify no custom fetch/proxy strips the query string).","Log the raw response body from the webhook POST to see what Discord actually returned (likely an error object).","Verify the webhook URL credential is valid and the webhook still exists (a deleted webhook can yield unexpected JSON from intermediaries).","Retry the upload; transient Discord API issues can produce malformed responses.","Check for a man-in-the-middle proxy/enterprise TLS inspection altering the response and bypass it."],"exampleFix":"// debugging before the parse call in upload()\nconst raw = await response.json();\nconsole.log(\"discord webhook response:\", JSON.stringify(raw));\nconst message = parseMessage(raw);","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isDiscordMessage(v: unknown): v is { id: string; attachments: unknown[] } {\n\treturn !!v && typeof v === \"object\" && typeof (v as Record<string, unknown>).id === \"string\"\n\t\t&& 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(\"message ID\")) {\n\t\t// log raw body and retry or fall back to another destination\n\t}\n\tthrow err;\n}","preventionTips":["Always execute webhooks with ?wait=true so Discord returns the full message object.","Log raw response bodies when a destination upload fails to speed diagnosis.","Validate webhook credentials before configuring the discord destination.","Avoid proxies/fetch wrappers that alter response bodies for API traffic."],"tags":["discord","webhook","response-validation","missing-field"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}