sipeed/picoclaw · error · ErrTemporary
slack_webhook: network error: %w
Error message
slack_webhook: network error: %w
What it means
Raised when the HTTP client fails to deliver the POST to the Slack webhook (DNS failure, connection refused, TLS error, timeout). The raw error is deliberately discarded — it can embed the webhook URL, which is a bearer secret — and the returned error wraps channels.ErrTemporary so callers classify it as transient. The channel manager (pkg/channels/manager.go:1606) retries ErrTemporary sends with exponential backoff.
Source
Thrown at pkg/channels/slack_webhook/slack_webhook.go:145
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("slack_webhook: failed to marshal payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target.WebhookURL.String(), bytes.NewReader(jsonData))
if err != nil {
return nil, fmt.Errorf("slack_webhook: failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
logger.ErrorCF("slack_webhook", "Failed to send message", map[string]any{
"target": targetName,
})
// Don't expose raw error - it may contain webhook URL secrets
return nil, fmt.Errorf("slack_webhook: network error: %w", channels.ErrTemporary)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
respText := strings.TrimSpace(string(respBody))
if respText == "" {
respText = http.StatusText(resp.StatusCode)
if respText == "" {
respText = "unknown error"
}
}
logger.ErrorCF("slack_webhook", "Slack API error", map[string]any{
"target": targetName,
"status": resp.StatusCode,
"response": respText,
})
sendErr := fmt.Errorf("status %d: %s", resp.StatusCode, respText)View on GitHub (pinned to 49183d7e8d)
Solutions
- Check egress from the host: curl -sS https://hooks.slack.com -o /dev/null -w '%{http_code}' should return 4xx quickly (any HTTP response proves connectivity).
- If behind a TLS-intercepting proxy, add its CA to the system trust store used by the process.
- Let the manager retry: errors wrapping ErrTemporary are retried with exponential backoff — do not disable retry for this channel.
- If using a custom http.Client, confirm its Timeout is generous enough (Slack webhook posts should complete in seconds).
Example fix
// before: treating every send error as permanent
if err := ch.Send(ctx, msg); err != nil {
return fmt.Errorf("give up: %w", err)
}
// after: honor the temporary classification
if err := ch.Send(ctx, msg); err != nil {
if errors.Is(err, channels.ErrTemporary) {
time.Sleep(backoff.Next()) // exponential backoff, then retry
return ch.Send(ctx, msg)
}
return err // permanent (e.g. ErrSendFailed)
} Defensive patterns
Strategy: retry
Validate before calling
// cheap reachability gate before a burst of sends
if _, err := http.Head("https://hooks.slack.com"); err != nil {
return fmt.Errorf("slack unreachable; queue messages instead of sending: %w", err)
} Type guard
// classify the returned sentinel
func isTemporary(err error) bool { return errors.Is(err, channels.ErrTemporary) } Try / catch
// manager-style: ErrTemporary -> exponential backoff and retry; raw cause is intentionally hidden (may contain the webhook secret)
if err := ch.Send(ctx, msg); err != nil {
if errors.Is(err, channels.ErrTemporary) {
return retryWithBackoff(ctx, func() error { return ch.Send(ctx, msg) })
}
return err // permanent
} Prevention
- Trust errors.Is(err, channels.ErrTemporary) — the raw network error is redacted on purpose
- Keep egress to hooks.slack.com allowed in firewalls/NetworkPolicy
- Add the CA of any TLS-intercepting proxy to the trust store
When it happens
Trigger: c.client.Do(req) returning an error: no DNS resolution for hooks.slack.com; firewall/proxy blocking egress; TLS interception with an untrusted CA; client timeout (the context deadline cancelling the request). It is NOT raised for HTTP error statuses — those go through the status-code path.
Common situations: Container/service without network egress; corporate MITM proxy whose CA is not in the trust store; transient ISP/DNS blips; Kubernetes NetworkPolicy blocking the webhook domain; system clock skew breaking TLS handshake.
Related errors
- after %d retries: %w
- read error response: %w
- read response: %w
- LLM call failed after retries: %w
- transcription request failed: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/604938872d93df11.
Report an issue: GitHub.