sipeed/picoclaw · error

status %d: %s

Error message

status %d: %s

What it means

Returned when Slack responds with HTTP >= 400 to the webhook POST. The body (capped at 512 bytes) or the status text is embedded as "status %d: %s", then passed through channels.ClassifySendError (pkg/channels/errutil.go:11): 429 wraps ErrRateLimit, 5xx wraps ErrTemporary, other 4xx wrap ErrSendFailed. So this one message covers three different retry policies, selected via errors.Is on the returned error.

Source

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

		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)
			if respText == "" {
				respText = "unknown error"
			}
		}
		logger.ErrorCF("slack_webhook", "Slack API error", map[string]any{
			"target":   targetName,
			"status":   resp.StatusCode,
			"response": respText,
		})
		sendErr := fmt.Errorf("status %d: %s", resp.StatusCode, respText)
		return nil, fmt.Errorf("slack_webhook: %w", channels.ClassifySendError(resp.StatusCode, sendErr))
	}

	logger.DebugCF("slack_webhook", "Message sent successfully", map[string]any{
		"target": targetName,
	})

	return nil, nil
}

func (c *SlackWebhookChannel) buildPayload(msg bus.OutboundMessage, target config.SlackWebhookTarget) map[string]any {
	payload := make(map[string]any)

	if target.Username != "" {
		payload["username"] = target.Username
	}
	if target.IconEmoji != "" {
		payload["icon_emoji"] = target.IconEmoji

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Read the numeric status in the message: 404/410/403 mean the webhook is dead — recreate the incoming webhook in the Slack app and update config.
  2. For 429, slow the send rate (the manager already backs off ErrRateLimit) and coalesce rapid notifications into fewer posts.
  3. For 400, reduce the message (the channel caps at 40000 chars — check attachments/blocks size) and re-send.
  4. For 5xx, retry via the ErrTemporary path with exponential backoff; check status.slack.com.

Example fix

// before: one-size-fits-all handling
if err := ch.Send(ctx, msg); err != nil { log.Fatal(err) }

// after: branch on the classified sentinel
if err := ch.Send(ctx, msg); err != nil {
    switch {
    case errors.Is(err, channels.ErrRateLimit):
        time.Sleep(2 * time.Second) // then retry
    case errors.Is(err, channels.ErrTemporary):
        retryWithBackoff(msg)       // 5xx
    default: // ErrSendFailed: 404/410/400 -> fix webhook or payload
        log.Printf("slack_webhook permanent: %v", err)
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// keep payloads inside Slack limits before sending
const slackMaxLen = 40000 // matches channels.WithMaxMessageLength(40000)
if len(msg.Text) > slackMaxLen { msg.Text = msg.Text[:slackMaxLen] }

Type guard

// sentinel discrimination after the call
func classify(err error) string {
    switch {
    case errors.Is(err, channels.ErrRateLimit): return "rate-limit"
    case errors.Is(err, channels.ErrTemporary): return "temporary"
    case errors.Is(err, channels.ErrSendFailed): return "permanent"
    }
    return "unknown"
}

Try / catch

if err := ch.Send(ctx, msg); err != nil {
    switch {
    case errors.Is(err, channels.ErrRateLimit): // 429
        time.Sleep(2 * time.Second); retry(ch, msg)
    case errors.Is(err, channels.ErrTemporary): // 5xx
        retryWithBackoff(ch, msg)
    default: // 404/410/400 — webhook dead or payload rejected
        log.Printf("slack_webhook permanent failure: %v", err)
    }
}

Prevention

When it happens

Trigger: 400 = malformed payload or too-large message (channel max length is 40000); 403 = webhook revoked; 404 = webhook deleted or channel removed; 410 = webhook gone; 429 = rate-limited (Slack incoming webhooks are limited to 1/sec); 5xx = Slack incident. The final error is prefixed "slack_webhook: " and wraps the classified sentinel.

Common situations: Deleting/recreating the Slack app invalidates old webhook URLs (404/410); bursts of notifications tripping the 1-per-second webhook limit (429); oversized messages with markdown/attachments exceeding Slack limits (400); Slack-side incidents producing 5xx.

Related errors


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