sipeed/picoclaw · error

slack_webhook: failed to create request: %w

Error message

slack_webhook: failed to create request: %w

What it means

The send path builds the POST request with http.NewRequestWithContext against the configured webhook URL; if that fails the error is wrapped as "slack_webhook: failed to create request". NewRequestWithContext re-parses the URL and validates the method; failure means the URL string, though it passed the constructor's url.Parse, contains something http.NewRequest rejects (e.g. control characters, invalid percent-encoding) or the context is nil.

Source

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

	if !ok {
		logger.WarnCF("slack_webhook", "Unknown target, falling back to default", map[string]any{
			"requested": msg.ChatID,
			"using":     "default",
		})
		target = c.config.Webhooks["default"]
		targetName = "default"
	}

	payload := c.buildPayload(msg, target)

	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)

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Recreate the channel (or re-run URL validation) whenever webhook_url changes — do not mutate config in place on a live channel.
  2. Normalize the URL once at construction (strip spaces/control chars, re-encode) and store the parsed *url.URL, then pass URL.String() to NewRequestWithContext.
  3. Check the wrapped *url.Error for the exact offending reason and fix the character it names.

Example fix

// before
req, err := http.NewRequestWithContext(ctx, http.MethodPost, rawURL, body)

// after: validate + normalize once, reuse the parsed URL
parsed, err := url.Parse(strings.TrimSpace(rawURL))
if err != nil { return nil, fmt.Errorf("bad webhook url: %w", err) }
c.parsedURL = parsed // reuse for every send
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.parsedURL.String(), body)
Defensive patterns

Strategy: validation

Validate before calling

// normalize and store the parsed URL once; validate on config change
func normalizeWebhookURL(raw string) (*url.URL, error) {
    u, err := url.Parse(strings.Map(func(r rune) rune {
        if r < 32 { return -1 }
        return r
    }, strings.TrimSpace(raw)))
    if err != nil || u.Scheme == "" || u.Host == "" { return nil, fmt.Errorf("invalid webhook url") }
    return u, nil
}

Try / catch

// Go: if err wraps *url.Error via errors.As -> the reason names the bad character; recreate the channel with a clean URL

Prevention

When it happens

Trigger: URL mutated after channel construction (hot-reloaded config that skipped constructor validation); URL containing a space or control character that survives url.Parse but is rejected when building the request; passing a nil ctx to Send. Note: an empty URL cannot reach here — the constructor blocks it.

Common situations: Config hot-reload replacing webhook_url without re-running NewSlackWebhookChannel validation; URLs pasted with trailing control characters that some parsers tolerate; forks changing the URL field between construction and send.

Related errors


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