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

  1. 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).
  2. Re-copy the webhook URL from the provider (Slack/Discord/Teams app config) and update the target.
  3. Confirm the payload schema matches the provider (the builder constructs provider-specific JSON; mixing providers and URLs fails).
  4. Add retry with backoff for 429/5xx; do not retry 4xx except 428.
  5. 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

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


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/3e95f40827faeaac. Report an issue: GitHub.