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

  1. 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).
  2. If behind a TLS-intercepting proxy, add its CA to the system trust store used by the process.
  3. Let the manager retry: errors wrapping ErrTemporary are retried with exponential backoff — do not disable retry for this channel.
  4. 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

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


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/604938872d93df11. Report an issue: GitHub.