sipeed/picoclaw · error

slack_webhook: webhook %q has empty webhook_url

Error message

slack_webhook: webhook %q has empty webhook_url

What it means

While validating each entry in the webhooks map, NewSlackWebhookChannel rejects a target whose webhook_url is the empty string (after SecureString unwrap). The map itself is well-formed, but one named target carries no URL, so any send addressed to it (or a fallback that resolves to it) would have no destination.

Source

Thrown at pkg/channels/slack_webhook/slack_webhook.go:49

// NewSlackWebhookChannel creates a new Slack webhook channel.
func NewSlackWebhookChannel(
	bc *config.Channel,
	cfg *config.SlackWebhookSettings,
	bus *bus.MessageBus,
) (*SlackWebhookChannel, error) {
	if len(cfg.Webhooks) == 0 {
		return nil, fmt.Errorf("slack_webhook: at least one webhook target is required")
	}

	if _, hasDefault := cfg.Webhooks["default"]; !hasDefault {
		return nil, fmt.Errorf("slack_webhook: a 'default' webhook target is required")
	}

	for name, target := range cfg.Webhooks {
		webhookURL := target.WebhookURL.String()
		if webhookURL == "" {
			return nil, fmt.Errorf("slack_webhook: webhook %q has empty webhook_url", name)
		}
		parsed, err := url.Parse(webhookURL)
		if err != nil {
			return nil, fmt.Errorf("slack_webhook: webhook %q has invalid URL format: %w", name, err)
		}
		if !strings.EqualFold(parsed.Scheme, "https") {
			return nil, fmt.Errorf("slack_webhook: webhook %q must use HTTPS (got %q)", name, parsed.Scheme)
		}
	}

	base := channels.NewBaseChannel(
		"slack_webhook",
		cfg,
		bus,
		[]string{"*"},
		channels.WithMaxMessageLength(40000),
	)

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Fill in webhook_url with the full https://hooks.slack.com/services/T…/B…/xxx URL for every named target.
  2. If the URL comes from a secret/env reference, verify the secret resolves to a non-empty value on this host.
  3. Remove targets you do not actually use instead of leaving empty entries.

Example fix

# before
webhooks:
  default:
    webhook_url: ""
  alerts: {}

# after
webhooks:
  default:
    webhook_url: "https://hooks.slack.com/services/T000/B000/abc"
  alerts:
    webhook_url: "https://hooks.slack.com/services/T000/B000/def"
Defensive patterns

Strategy: validation

Validate before calling

// validate every target before constructing the channel
for name, t := range cfg.Webhooks {
    if strings.TrimSpace(t.WebhookURL.String()) == "" {
        return fmt.Errorf("webhook %q has empty webhook_url", name)
    }
}

Type guard

// non-empty guard per target
func targetReady(t config.SlackWebhookTarget) bool {
    return strings.TrimSpace(t.WebhookURL.String()) != ""
}

Try / catch

// Go: constructor error naming the offending webhook %q — fix that entry; do not catch at runtime

Prevention

When it happens

Trigger: A webhooks entry present with other fields set but webhook_url missing or empty; a secret reference that resolves to empty (missing env var, unset secret); YAML writing webhook_url: with no value (parses as null/empty).

Common situations: Placeholder entries added while waiting for the real webhook URL; secrets loaded from a vault that returns empty on ACL failure; trailing whitespace stripping issues leaving an effectively empty value; entry half-created during config refactoring.

Related errors


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