affaan-m/ECC · error · anyhow::Error
{} returned {}
Error message
{} returned {} What it means
Thrown by send_webhook_request (non-test build) when the configured webhook target URL returns an HTTP status that is not a 2xx success. The POST completed but the endpoint rejected or errored, so the webhook delivery is treated as failed.
Source
Thrown at ecc2/src/notifications.rs:419
Ok(())
}
#[cfg(not(test))]
fn send_webhook_request(target: &WebhookTarget, payload: serde_json::Value) -> Result<()> {
let agent = ureq::Agent::config_builder()
.timeout_connect(Some(std::time::Duration::from_secs(5)))
.timeout_recv_response(Some(std::time::Duration::from_secs(5)))
.build()
.new_agent();
let response = agent
.post(&target.url)
.send_json(payload)
.with_context(|| format!("POST {}", target.url))?;
if response.status().is_success() {
Ok(())
} else {
anyhow::bail!("{} returned {}", target.url, response.status());
}
}
#[cfg(test)]
fn send_webhook_request(_target: &WebhookTarget, _payload: serde_json::Value) -> Result<()> {
Ok(())
}
fn sanitize_osascript(value: &str) -> String {
value
.replace('\\', "")
.replace('"', "\u{201C}")
.replace('\n', " ")
}
#[cfg(test)]
mod tests {
use super::{View on GitHub (pinned to 01e15490f0)
Solutions
- Read the status code in the message: 401/403 means auth/URL is bad (regenerate the webhook URL); 400 means payload mismatch; 429 means rate limit (slow down or batch); 5xx means the endpoint is unhealthy (retry).
- Re-copy the webhook URL from the provider (Slack/Discord/Teams app config) and update the target.
- Confirm the payload schema matches the provider (the builder constructs provider-specific JSON; mixing providers and URLs fails).
- Add retry with backoff for 429/5xx; do not retry 4xx except 428.
- If behind a corporate proxy, ensure the proxy is configured and not returning an error page.
Example fix
# before: revoked Slack webhook URL ecc2 notify set-webhook --url https://hooks.slack.com/services/T000/B000/OLD_TOKEN # after: regenerate the webhook in Slack and use the new URL ecc2 notify set-webhook --url https://hooks.slack.com/services/T000/B000/NEW_TOKEN
Defensive patterns
Strategy: retry
Validate before calling
fn webhook_url_looks_valid(url: &str) -> bool {
url.starts_with("https://") || url.starts_with("http://")
}
// pre-flight: only POST if the URL is well-formed and the provider matches the payload builder Try / catch
match send_webhook_request(target, payload) {
Ok(()) => (),
Err(e) => {
let msg = format!("{e:#}");
if msg.contains(" 429 ") || msg.contains(" returned 5") {
// transient: schedule a retry with backoff
schedule_retry(target, payload);
} else {
// 4xx (except 429): do not retry; surface for human fix
log::error!("webhook permanently failed: {msg}");
}
}
} Prevention
- Regenerate and rotate webhook URLs on a schedule; do not hardcode long-lived tokens.
- Match the payload schema to the provider whose URL you configured.
- Retry 429/5xx with exponential backoff; never retry other 4xx.
- Add a health check that does a lightweight test POST when the URL changes.
When it happens
Trigger: A webhook target (Slack, Discord, Teams, or custom) returns 4xx (bad URL, bad auth, malformed payload) or 5xx (endpoint down, rate limited with 429). Examples: a Slack incoming webhook URL that was revoked (403/404), a Discord webhook rate limit (429), or a custom endpoint that expects a different payload schema (400).
Common situations: Expired/rotated webhook URL; wrong payload format for the provider; destination service outage; rate limiting; network proxy returning 502/504; URL typo.
Related errors
- HTTP ${res.status}
- open failed (HTTP ${res.statusCode})
- fetch %s: %w
- Unexpected EOF while reading HTTP request
- HTTP request headers too large
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/3e95f40827faeaac.
Report an issue: GitHub.