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

  1. 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.
  2. Verify chat_id is the oc_-prefixed chat ID taken from the inbound event, not an open_id/user ID.
  3. Validate the card JSON against the Feishu card builder (element types, version field) before sending.
  4. 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

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


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