{"record":{"id":"6bd2bc9cb5f314f7","repo":"sipeed/picoclaw","slug":"slack-webhook-webhook-q-has-empty-webhook-url","errorCode":null,"errorMessage":"slack_webhook: webhook %q has empty webhook_url","messagePattern":"slack_webhook: webhook %q has empty webhook_url","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/channels/slack_webhook/slack_webhook.go","lineNumber":49,"sourceCode":"\n// NewSlackWebhookChannel creates a new Slack webhook channel.\nfunc NewSlackWebhookChannel(\n\tbc *config.Channel,\n\tcfg *config.SlackWebhookSettings,\n\tbus *bus.MessageBus,\n) (*SlackWebhookChannel, error) {\n\tif len(cfg.Webhooks) == 0 {\n\t\treturn nil, fmt.Errorf(\"slack_webhook: at least one webhook target is required\")\n\t}\n\n\tif _, hasDefault := cfg.Webhooks[\"default\"]; !hasDefault {\n\t\treturn nil, fmt.Errorf(\"slack_webhook: a 'default' webhook target is required\")\n\t}\n\n\tfor name, target := range cfg.Webhooks {\n\t\twebhookURL := target.WebhookURL.String()\n\t\tif webhookURL == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"slack_webhook: webhook %q has empty webhook_url\", name)\n\t\t}\n\t\tparsed, err := url.Parse(webhookURL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"slack_webhook: webhook %q has invalid URL format: %w\", name, err)\n\t\t}\n\t\tif !strings.EqualFold(parsed.Scheme, \"https\") {\n\t\t\treturn nil, fmt.Errorf(\"slack_webhook: webhook %q must use HTTPS (got %q)\", name, parsed.Scheme)\n\t\t}\n\t}\n\n\tbase := channels.NewBaseChannel(\n\t\t\"slack_webhook\",\n\t\tcfg,\n\t\tbus,\n\t\t[]string{\"*\"},\n\t\tchannels.WithMaxMessageLength(40000),\n\t)\n","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/slack_webhook/slack_webhook.go#L31-L67","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Fill in webhook_url with the full https://hooks.slack.com/services/T…/B…/xxx URL for every named target.","If the URL comes from a secret/env reference, verify the secret resolves to a non-empty value on this host.","Remove targets you do not actually use instead of leaving empty entries."],"exampleFix":"# before\nwebhooks:\n  default:\n    webhook_url: \"\"\n  alerts: {}\n\n# after\nwebhooks:\n  default:\n    webhook_url: \"https://hooks.slack.com/services/T000/B000/abc\"\n  alerts:\n    webhook_url: \"https://hooks.slack.com/services/T000/B000/def\"","handlingStrategy":"validation","validationCode":"// validate every target before constructing the channel\nfor name, t := range cfg.Webhooks {\n    if strings.TrimSpace(t.WebhookURL.String()) == \"\" {\n        return fmt.Errorf(\"webhook %q has empty webhook_url\", name)\n    }\n}","typeGuard":"// non-empty guard per target\nfunc targetReady(t config.SlackWebhookTarget) bool {\n    return strings.TrimSpace(t.WebhookURL.String()) != \"\"\n}","tryCatchPattern":"// Go: constructor error naming the offending webhook %q — fix that entry; do not catch at runtime","preventionTips":["Never commit placeholder targets with empty URLs","If URLs come from a vault, assert non-empty resolution at startup","Delete unused targets instead of leaving stubs"],"tags":["slack-webhook","config","webhook","validation"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}