{"record":{"id":"604938872d93df11","repo":"sipeed/picoclaw","slug":"slack-webhook-network-error-w","errorCode":null,"errorMessage":"slack_webhook: network error: %w","messagePattern":"slack_webhook: network error: %w","errorType":"exception","errorClass":"ErrTemporary","httpStatus":null,"severity":"error","filePath":"pkg/channels/slack_webhook/slack_webhook.go","lineNumber":145,"sourceCode":"\n\tjsonData, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"slack_webhook: failed to marshal payload: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, target.WebhookURL.String(), bytes.NewReader(jsonData))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"slack_webhook: failed to create request: %w\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\tlogger.ErrorCF(\"slack_webhook\", \"Failed to send message\", map[string]any{\n\t\t\t\"target\": targetName,\n\t\t})\n\t\t// Don't expose raw error - it may contain webhook URL secrets\n\t\treturn nil, fmt.Errorf(\"slack_webhook: network error: %w\", channels.ErrTemporary)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\trespBody, _ := io.ReadAll(io.LimitReader(resp.Body, 512))\n\t\trespText := strings.TrimSpace(string(respBody))\n\t\tif respText == \"\" {\n\t\t\trespText = http.StatusText(resp.StatusCode)\n\t\t\tif respText == \"\" {\n\t\t\t\trespText = \"unknown error\"\n\t\t\t}\n\t\t}\n\t\tlogger.ErrorCF(\"slack_webhook\", \"Slack API error\", map[string]any{\n\t\t\t\"target\":   targetName,\n\t\t\t\"status\":   resp.StatusCode,\n\t\t\t\"response\": respText,\n\t\t})\n\t\tsendErr := fmt.Errorf(\"status %d: %s\", resp.StatusCode, respText)","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/slack_webhook/slack_webhook.go#L127-L163","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before: treating every send error as permanent\nif err := ch.Send(ctx, msg); err != nil {\n    return fmt.Errorf(\"give up: %w\", err)\n}\n\n// after: honor the temporary classification\nif err := ch.Send(ctx, msg); err != nil {\n    if errors.Is(err, channels.ErrTemporary) {\n        time.Sleep(backoff.Next()) // exponential backoff, then retry\n        return ch.Send(ctx, msg)\n    }\n    return err // permanent (e.g. ErrSendFailed)\n}","handlingStrategy":"retry","validationCode":"// cheap reachability gate before a burst of sends\nif _, err := http.Head(\"https://hooks.slack.com\"); err != nil {\n    return fmt.Errorf(\"slack unreachable; queue messages instead of sending: %w\", err)\n}","typeGuard":"// classify the returned sentinel\nfunc isTemporary(err error) bool { return errors.Is(err, channels.ErrTemporary) }","tryCatchPattern":"// manager-style: ErrTemporary -> exponential backoff and retry; raw cause is intentionally hidden (may contain the webhook secret)\nif err := ch.Send(ctx, msg); err != nil {\n    if errors.Is(err, channels.ErrTemporary) {\n        return retryWithBackoff(ctx, func() error { return ch.Send(ctx, msg) })\n    }\n    return err // permanent\n}","preventionTips":["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"],"tags":["slack-webhook","network","retry","transient","secrets"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}