affaan-m/ECC · error · Error
invalid Discord webhook URL
Error message
invalid Discord webhook URL
What it means
normalizeDiscordWebhookUrl parses the webhook URL and throws 'invalid Discord webhook URL' when new URL() fails — i.e. the value is not an absolute, well-formed URL at all. This is the first of three validation stages: URL parseability, then host/protocol constraints, then path shape.
Solutions
- Set DISCORD_WEBHOOK_URL to the full webhook URL copied from Discord (Server Settings -> Integrations -> Webhooks -> Copy URL), including 'https://'.
- Trim whitespace/newlines: normalizeDiscordWebhookUrl(value.trim().replace(/\s+/g, '')).
- Verify the secret in the CI workflow was saved with the complete URL and not truncated by the secret editor.
- Check locally: node -e "new URL(process.env.DISCORD_WEBHOOK_URL)" should not throw.
Example fix
// before
const url = normalizeDiscordWebhookUrl('discord.com/api/webhooks/123/abc');
// after
const url = normalizeDiscordWebhookUrl('https://discord.com/api/webhooks/123/abc'); Defensive patterns
Strategy: validation
Validate before calling
function looksLikeWebhookUrl(v) {
if (typeof v !== 'string') return false;
try { return new URL(v.trim().replace(/\s+/g, '')).protocol === 'https:'; } catch { return false; }
}
if (!looksLikeWebhookUrl(process.env.DISCORD_WEBHOOK_URL)) throw new Error('DISCORD_WEBHOOK_URL must be an absolute https URL'); Type guard
const isParseableUrl = (v) => { try { new URL(typeof v === 'string' ? v.trim() : ''); return true; } catch { return false; } }; Try / catch
try {
const url = normalizeDiscordWebhookUrl(env.DISCORD_WEBHOOK_URL);
} catch (err) {
if (err.message === 'invalid Discord webhook URL') {
console.error('DISCORD_WEBHOOK_URL must be a full https://discord.com/api/webhooks/... URL');
process.exit(1);
}
throw err;
} Prevention
- Copy the webhook URL verbatim from Discord's Integrations UI, scheme included.
- Trim whitespace and strip quotes/newlines when the value comes from YAML secrets.
- Check the secret is not truncated in the CI secret editor.
- Run a node one-liner new URL(...) check locally before committing config.
When it happens
Trigger: main() passes an empty/undefined webhook value, a bare token without scheme ('discord.com/api/webhooks/...'), a URL with a typo in the scheme ('https:/discord.com/...'), or a non-URL string from an env var or secret store.
Common situations: DISCORD_WEBHOOK_URL env var unset or quoted incorrectly in workflow YAML; secret pasted without the leading https://; multiline paste introducing whitespace/newlines into the string.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- download requires HTTPS on an approved fal.media host
- invalid interaction id/token
- Invalid managed hook handler at
- must be an HTTPS URL without embedded credentials
- -32602
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/d76721727023c844.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/discord/announcement-core.mjs:58
description,
url: String(url || ''),
footer: { text: footer },
}],
};
}
export function findDiscordReceipt(messages, key) {
const discussionId = String(key).split(':').at(-1);
return messages.find(message => message.embeds?.some(embed => embed.footer?.text === `ecc:${discussionId}`)) || null;
}
export function normalizeDiscordWebhookUrl(value) {
const raw = String(value || '').trim();
let parsed;
try {
parsed = new URL(raw);
} catch {
throw new Error('invalid Discord webhook URL');
}
if (parsed.protocol !== 'https:' || parsed.hostname !== 'discord.com' || parsed.port || parsed.username || parsed.password || parsed.search || parsed.hash) {
throw new Error('invalid Discord webhook URL');
}
if (!/^\/api\/webhooks\/\d{10,25}\/[A-Za-z0-9._-]{20,}$/.test(parsed.pathname)) {
throw new Error('invalid Discord webhook URL');
}
parsed.search = '?wait=true';
return parsed.toString();
}
export function discussionReceiptMarker(key) {
return `<!-- ecc-discord-receipt:${createHash('sha256').update(String(key)).digest('hex').slice(0, 32)} -->`;
}
export function findDiscussionReceipt(comments, marker) {
const trusted = comments.filter(comment => (
['github-actions', 'github-actions[bot]'].includes(comment?.author?.login)View on GitHub (pinned to 8321021c54)