{"record":{"id":"98e41d062b0f2a4d","repo":"can1357/oh-my-pi","slug":"discord-attachment-url-is-invalid","errorCode":null,"errorMessage":"Discord attachment URL is invalid","messagePattern":"Discord attachment URL is invalid","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/uploaders-discord.ts","lineNumber":66,"sourceCode":"function 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}\n\n/** Create the built-in Discord webhook uploader, or `null` for another destination. */\nexport function createDiscordUploader(\n\tdestination: BlobDestinationId,\n\tconfig: DestinationRuntimeConfig,","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/uploaders-discord.ts#L48-L84","documentation":"The Discord uploader extracts an attachment URL from a message payload and validates it before use. If the URL cannot be parsed by the URL constructor or does not use the https: protocol, this error is thrown. It guards against malformed or insecure attachment links returned by Discord.","triggerScenarios":"The first attachment in the message has no `url` string field; the value is not a valid absolute URL (e.g. relative path or garbage); or the URL parses but uses a protocol other than https (http:, cdn:, data:).","commonSituations":"Consuming Discord message objects from a custom or mocked payload that omits attachment.url; proxy responses rewriting attachment URLs to http; hand-built test fixtures with relative paths; Discord API version changes altering the attachment shape.","solutions":["Log the attachment object and confirm `url` is present and a string before calling the uploader","Ensure the attachment URL is absolute and starts with https:// (Discord CDN URLs normally do)","If reconstructing payloads, copy `attachment.url` verbatim from the Discord API response instead of building it manually","Catch this error and fall back to another uploader or surface the message payload for debugging"],"exampleFix":"// before\nconst attachmentUrl = attachment.url ?? `/files/${attachment.id}`;\n// after\nconst attachmentUrl = attachment.url;\nif (!attachmentUrl || !attachmentUrl.startsWith(\"https://\")) {\n  throw new Error(`attachment ${attachment.id} has no https URL`);\n}","handlingStrategy":"validation","validationCode":"function hasHttpsUrl(attachment) {\n  return typeof attachment?.url === \"string\" && URL.canParse(attachment.url) && new URL(attachment.url).protocol === \"https:\";\n}\nif (!hasHttpsUrl(message.attachments[0])) throw new Error(\"attachment has no valid https URL\");","typeGuard":"function isDiscordAttachment(value) {\n  return typeof value === \"object\" && value !== null && typeof value.url === \"string\" && value.url.startsWith(\"https://\");\n}","tryCatchPattern":"try {\n  await broker.publish(request);\n} catch (err) {\n  if (err.message === \"Discord attachment URL is invalid\") {\n    logger.warn(\"Discord attachment unusable\", { attachment: firstAttachment });\n    return null; // or fall back to another broker\n  }\n  throw err;\n}","preventionTips":["Copy attachment.url verbatim from Discord API responses; never synthesize it","Assert https:// scheme on attachment URLs before upload","Keep Discord API payload shapes in sync with the library's expectations","Log the raw attachment object when this error fires for debugging"],"tags":["validation","url-parsing","discord","https"],"backgroundTag":"invalid-url","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}