{"record":{"id":"7395f26921a83fa5","repo":"can1357/oh-my-pi","slug":"discord-attachment-did-not-include-a-url","errorCode":null,"errorMessage":"Discord attachment did not include a URL","messagePattern":"Discord attachment did not include a URL","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/uploaders-discord.ts","lineNumber":61,"sourceCode":"\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;\n\t\tif (Number.isSafeInteger(expiresAt) && expiresAt > 0) return expiresAt;\n\t}\n\treturn now + FALLBACK_LIFETIME_MS;\n}","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/uploaders-discord.ts#L43-L79","documentation":"Thrown by parseMessage() when Discord's response has a message id and a non-empty attachments array, but attachments[0].url is missing or not a string. The uploader needs this CDN URL as the publication URL for the uploaded blob. Discord message attachment objects always carry a `url` in normal operation, so its absence signals an abnormal or hostile response shape.","triggerScenarios":"attachments[0] is an object lacking a `url` string field — e.g. a partial attachment record from a degraded API response, a proxy returning trimmed JSON, or Discord schema changes in preview/experimental API versions.","commonSituations":"Using an unofficial proxy of the Discord API that strips fields; Discord schema drift; attaching via a thread the uploader cannot fully resolve; intercepting fetch wrappers that cache and reshape responses.","solutions":["Log the full attachments[0] object from the response to see which fields Discord returned.","Verify requests go directly to https://discord.com/api/v10 with no reshaping proxy.","Pin/verify the API version (v10) and check Discord's changelog for attachment schema changes.","Retry the upload; partial records are often transient.","If persistently reproducible, capture the response and report/check Discord status."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function attachmentHasUrl(v: unknown): v is { url: string } {\n\treturn !!v && typeof v === \"object\" && typeof (v as Record<string, unknown>).url === \"string\";\n}","tryCatchPattern":"try {\n\tconst message = parseMessage(await response.json());\n} catch (err) {\n\tif (err instanceof Error && err.message.includes(\"did not include a URL\")) {\n\t\t// capture attachments[0] for diagnosis; fall back to another destination or retry\n\t}\n\tthrow err;\n}","preventionTips":["Route Discord API traffic directly to discord.com without reshaping proxies.","Pin the Discord API version (v10) and watch for schema changes.","Log full attachment objects when parsing fails to spot partial records.","Retry transient failures before surfacing errors to users."],"tags":["discord","webhook","attachment","missing-field","response-validation"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}