sipeed/picoclaw · error · ErrSendFailed
chat ID is empty: %w
Error message
chat ID is empty: %w
What it means
FeishuChannel.Send rejects an OutboundMessage whose ChatID is empty, wrapping the permanent ErrSendFailed sentinel. Feishu's create-message API needs a receive_id (chat_id), so an empty one can never succeed - the guard prevents a wasted API round-trip. Not retryable.
Source
Thrown at pkg/channels/feishu/feishu_64.go:161
c.mu.Unlock()
if c.progress != nil {
c.progress.StopAll()
}
c.SetRunning(false)
logger.InfoC("feishu", "Feishu channel stopped")
return nil
}
// Send sends a message using Interactive Card format for markdown rendering.
// Falls back to plain text message if card sending fails (e.g., table limit exceeded).
func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
if !c.IsRunning() {
return nil, channels.ErrNotRunning
}
if msg.ChatID == "" {
return nil, fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
}
isToolFeedback := outboundMessageIsToolFeedback(msg)
if isToolFeedback {
if msgID, handled, err := c.progress.Update(ctx, msg.ChatID, msg.Content); handled {
if err != nil {
// Feishu can fall back to plain text for a previous progress
// message, and those messages cannot be patched through the card
// edit API. Drop the stale tracker and recreate the progress
// message so later tool feedback is not blocked.
c.resetTrackedToolFeedbackAfterEditFailure(ctx, msg.ChatID)
} else {
return []string{msgID}, nil
}
}
} else {
if msgIDs, handled := c.FinalizeToolFeedbackMessage(ctx, msg); handled {
return msgIDs, nilView on GitHub (pinned to 49183d7e8d)
Solutions
- Find where the OutboundMessage was produced and ensure the inbound event's chat_id (event.message.chat_id) is copied into ChatID
- Add a guard in the dispatcher/router: drop or log messages with an empty ChatID before they reach the channel
- If it happens after adding a bot to a new chat type (p2p/group), verify the event parser handles that chat type
Example fix
// before
bus.Publish(bus.OutboundMessage{Content: reply}) // ChatID forgotten
// after
chatID := ev.Message.ChatId
if chatID == nil || *chatID == "" {
logger.Warn("no chat_id on event; dropping reply")
return
}
bus.Publish(bus.OutboundMessage{ChatID: *chatID, Content: reply}) Defensive patterns
Strategy: validation
Validate before calling
func outboundAddressable(msg bus.OutboundMessage) bool {
return strings.TrimSpace(msg.ChatID) != ""
} Type guard
null
Try / catch
if err := ch.Send(ctx, msg); err != nil {
if errors.Is(err, channels.ErrSendFailed) && msg.ChatID == "" {
logger.Warn("dropped unaddressed outbound message", "origin", msg.Origin)
return nil // routing bug upstream: do not retry
}
return err
} Prevention
- Validate ChatID at message construction, not at the channel boundary
- Add a router invariant: every reply carries the inbound event's chat_id
- Property-test the message builder so empty-ChatID messages cannot be published
- Alert on this error - it always indicates an upstream bug, never transient conditions
When it happens
Trigger: The bus delivers bus.OutboundMessage{ChatID: ""} to a running Feishu channel: upstream router failed to map the inbound event to a reply chat, or a programmatically built message skipped the ChatID field.
Common situations: Webhook/event handler did not extract chat_id from the Feishu event payload; reply routing table miss; test helper constructs OutboundMessage without a chat; broadcast fan-out path with an empty target.
Related errors
- no target channel/chat available
- feishu app_id or app_secret is empty
- no media store available: %w
- feishu image upload api error (code=%d msg=%s)
- feishu file upload api error (code=%d msg=%s)
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/e79f42d0e20ea8f0.
Report an issue: GitHub.