sipeed/picoclaw · error · ErrTemporary
feishu api error (code=%d msg=%s): %w
Error message
feishu api error (code=%d msg=%s): %w
What it means
Feishu (Lark) Open API rejected an interactive card message: the HTTP call completed but resp.Success() is false, so the body carried a non-zero business code (resp.Code/resp.Msg). The channel wraps channels.ErrTemporary, so the outbound queue retries with exponential backoff (500ms*2^attempt, max 8s). Before returning it calls invalidateTokenOnAuthError, which clears the cached tenant_access_token when code is 99991663 (see pkg/channels/feishu/feishu_64.go:1261).
Source
Thrown at pkg/channels/feishu/feishu_64.go:1092
// 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).
func (c *FeishuChannel) sendText(ctx context.Context, chatID, text string) (string, error) {
content, _ := json.Marshal(map[string]string{"text": text})
req := larkim.NewCreateMessageReqBuilder().
ReceiveIdType(larkim.CreateMessageV1ReceiveIDTypeChatId).View on GitHub (pinned to 49183d7e8d)
Solutions
- Read the code=%d in the message text: 99991663 -> fix app credentials (token cache is auto-cleared, next attempt fetches a fresh token); 230001 -> add the bot to the target chat and grant im:message scope; 99991400 -> throttle card sends.
- Verify chat_id is the oc_-prefixed chat ID taken from the inbound event, not an open_id/user ID.
- Validate the card JSON against the Feishu card builder (element types, version field) before sending.
- If it persists, copy msg= verbatim into the Feishu open-platform error-code lookup.
Example fix
// before
_, err := ch.Send(ctx, msg) // error surfaces as 'feishu api error (code=230001 ...)'
// after: classify and act on the wrapped sentinel
if _, err := ch.Send(ctx, msg); err != nil {
if errors.Is(err, channels.ErrTemporary) {
// queue will retry with backoff; inspect text for code= to find root cause
if strings.Contains(err.Error(), "code=230001") {
// permanent root cause: stop retrying, add bot to chat
}
}
} Defensive patterns
Strategy: retry
Validate before calling
if msg.ChatID == "" || !strings.HasPrefix(msg.ChatID, "oc_") {
return fmt.Errorf("invalid feishu chat_id %q: expected oc_-prefixed chat id", msg.ChatID)
}
if _, jerr := json.Marshal(card); jerr != nil {
return fmt.Errorf("card content not serializable: %w", jerr)
} Type guard
func isTemporarySendErr(err error) bool {
return errors.Is(err, channels.ErrTemporary)
} Try / catch
if _, err := ch.Send(ctx, msg); err != nil {
switch {
case errors.Is(err, channels.ErrTemporary):
// scheduler retries with backoff; parse 'code=' in text for 99991663/230001/99991400
default:
// permanent — do not retry
}
} Prevention
- Keep the bot a member of every target chat
- Grant im:message scope before first send
- Rate-limit card sends below the tenant QPS cap
- Alert when code=99991663 appears more than once — credential rotation may be needed
When it happens
Trigger: Im.V1.Message.Create with msg_type=interactive fails: bot not a member of the target chat or missing im:message permission (230001), stale/invalid tenant_access_token (99991663), QPS/rate limit exceeded (99991400), non-existent chat_id (receive_id invalid), or card JSON that does not match the card schema.
Common situations: Bot was removed from the group; app_id/app_secret rotated so the cached token is stale; card schema change after Feishu deprecates elements; burst sends tripping tenant rate limits.
Related errors
- feishu send text: %w
- feishu text api error (code=%d msg=%s): %w
- feishu image upload api error (code=%d msg=%s)
- feishu image send 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/0dad970c40c6cf05.
Report an issue: GitHub.