sipeed/picoclaw · error · ErrTemporary
feishu send card: %w
Error message
feishu send card: %w
What it means
FeishuChannel.sendCard called lark SDK Im.V1.Message.Create (interactive card) and the SDK failed at the transport level; the wrapper returns the ErrTemporary sentinel. Note the SDK error itself is NOT wrapped - callers see only the retryable class and must infer/inspect the cause elsewhere. Retry policy per channels package: exponential backoff 500ms*2^attempt, max 8s.
Source
Thrown at pkg/channels/feishu/feishu_64.go:1087
return tag
}
return content + " " + tag
}
// sendCard sends an interactive card message to a chat.
func (c *FeishuChannel) sendCard(ctx context.Context, chatID, cardContent string) (string, error) {
req := larkim.NewCreateMessageReqBuilder().
ReceiveIdType(larkim.CreateMessageV1ReceiveIDTypeChatId).
Body(larkim.NewCreateMessageReqBodyBuilder().
ReceiveId(chatID).
MsgType(larkim.MsgTypeInteractive).
Content(cardContent).
Build()).
Build()
resp, err := c.client.Im.V1.Message.Create(ctx, req)
if err != nil {
return "", fmt.Errorf("feishu send card: %w", channels.ErrTemporary)
}
if !resp.Success() {
c.invalidateTokenOnAuthError(resp.Code)
return "", fmt.Errorf("feishu api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary)
}
logger.DebugCF("feishu", "Feishu card message sent", map[string]any{
"chat_id": chatID,
})
if resp.Data != nil && resp.Data.MessageId != nil {
return *resp.Data.MessageId, nil
}
return "", nil
}
// sendText sends a plain text message to a chat (fallback when card fails).View on GitHub (pinned to 49183d7e8d)
Solutions
- Retry with exponential backoff (up to 8s) - the sentinel deliberately marks this retryable
- If persistent, capture the underlying SDK error at the sendCard call site (it is currently discarded) to diagnose
- Shrink card content: split long tables or truncate to keep payloads lean
- Check network/proxy health to open.feishu.cn
Example fix
// before (pkg/channels/feishu/feishu_64.go) - cause discarded
if err != nil {
return "", fmt.Errorf("feishu send card: %w", channels.ErrTemporary)
}
// after - preserve the cause for diagnosis
if err != nil {
return "", fmt.Errorf("feishu send card: %w: %w", channels.ErrTemporary, err)
} Defensive patterns
Strategy: retry
Validate before calling
null
Type guard
func isTemporaryCardSend(err error) bool {
return err != nil && errors.Is(err, channels.ErrTemporary)
} Try / catch
var err error
for attempt := 0; attempt < 4; attempt++ {
_, err = sendViaChannel(ctx, msg)
if !isTemporaryCardSend(err) { break }
time.Sleep(min(500*time.Millisecond<<attempt, 8*time.Second))
}
return err Prevention
- Honor the sentinel's contract: exponential backoff, capped at 8s, limited attempts
- Trim oversized markdown tables before sending - big cards cause slow-request timeouts misread as transient
- Note the wrapper drops the SDK cause; add temporary logging at the sendCard site when diagnosing
- Stop retrying after the budget: sustained ErrTemporary signals an outage, not luck
When it happens
Trigger: POST create-message fails at the network layer while sending a card: timeout, connection reset, DNS failure, oversized card body stalling the request.
Common situations: Transient egress issues; very large markdown tables inflating card JSON past intermediary timeouts; burst sends saturating connections; Feishu edge blips.
Related errors
- feishu send media: %w
- feishu send text: %w
- after %d retries: %w
- LLM call failed after retries: %w
- transcription request failed: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/126c81831ad674d2.
Report an issue: GitHub.