slack

slack sends messages through Incoming Webhooks, with per-message identity overrides, retries, a separate status probe, and webhook URLs redacted from error text.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/slack"

Posts text to a Slack Incoming Webhook.

c, err := slack.New(webhookURL, "my-bot", ":rocket:", "", "#alerts")
err = c.Send(ctx, "deploy finished", "", "", "", "")

The webhook URL is a credential, and it appears in error messages

This is the part that separates the package from an inline http.Post.

A Slack Incoming Webhook URL contains the secret. Anyone holding https://hooks.slack.com/services/T00.../B00.../XXXX can post to that channel; there is no separate token.

Go’s net/http includes the request URL in transport error text. A DNS failure, a timeout, or a refused connection produces an error whose Error() string contains the full URL. Log that error and the credential is now in your log aggregation, your error tracker, and anywhere those get shipped.

The client wraps its errors so the configured address is replaced with REDACTED in the message, while Unwrap still returns the original error, so errors.Is and errors.As keep working:

type redactedError struct {
    msg string   // address replaced with REDACTED
    err error    // original, reachable via Unwrap
}

The same guard is in sleuth, which has the same exposure.

Per-message identity, with client defaults

New takes the default username, icon emoji, icon URL and channel. Send takes the same four again, and an empty value falls back to the client default:

c.Send(ctx, "routine", "", "", "", "")                    // all defaults
c.Send(ctx, "urgent", "alertbot", ":fire:", "", "#oncall") // overridden

One client therefore serves several notification kinds without constructing one per channel, which matters because the webhook URL is bound to a workspace rather than to a channel and the channel override is what routes the message.

Icon emoji and icon URL are separate parameters because Slack treats them as alternatives; supplying both lets Slack pick.

HealthCheck probes somewhere else

HealthCheck does not post to the webhook. It checks the Slack Status API by default, configurable through WithPingURL, and uses its own timeout through WithPingTimeout.

That separation is deliberate. A health check that exercised the real webhook would post a message to a human-visible channel every time a readiness probe ran. Probing a status endpoint answers whether Slack is reachable without producing traffic anyone has to read.

It also means a passing health check does not prove the webhook itself is valid. A revoked webhook fails at Send, not at HealthCheck.

Options

OptionEffect
WithTimeoutper-message request timeout
WithPingTimeouthealth-check timeout, separate from the above
WithPingURLwhere HealthCheck probes
WithHTTPClientinject a transport, or a mock
WithRetryAttemptsretries on a failed send

WithHTTPClient takes an HTTPClient interface rather than *http.Client, so a test can supply a mock and a service can supply an instrumented client from httpclient.

Notifications are not a delivery guarantee

Send is synchronous and returns an error when the post fails after its retries. Slack rate-limits Incoming Webhooks, and a burst of alerts can exhaust the retry budget.

Treat a notification path as best-effort and keep the authoritative record in your logs or metrics. A failed Send should not fail the operation that triggered it.

Example

c, err := slack.New("https://hooks.slack.com/services/...", "bot", ":robot_face:", "", "#general")
if err != nil {
    log.Fatal(err)
}

err = c.Send(context.Background(), "hello", "", "", "", "")

Full source is in example_slack_test.go. More runnable examples are on pkg.go.dev.

Dependencies

This package reaches no external module: it uses only the Go standard library.