sipeed/picoclaw · error · ErrTemporary

feishu text api error (code=%d msg=%s): %w

Error message

feishu text api error (code=%d msg=%s): %w

What it means

Feishu API returned a non-zero business code for the plain-text fallback send (used after the card send failed). Unlike the card path at line 1092, this branch does NOT call invalidateTokenOnAuthError, so a stale-token failure (99991663) on the text fallback keeps failing until the token naturally expires (~2 hours). The error still wraps channels.ErrTemporary, so it is retried with backoff.

Source

Thrown at pkg/channels/feishu/feishu_64.go:1124

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).
		Body(larkim.NewCreateMessageReqBodyBuilder().
			ReceiveId(chatID).
			MsgType(larkim.MsgTypeText).
			Content(string(content)).
			Build()).
		Build()

	resp, err := c.client.Im.V1.Message.Create(ctx, req)
	if err != nil {
		return "", fmt.Errorf("feishu send text: %w", channels.ErrTemporary)
	}

	if !resp.Success() {
		return "", fmt.Errorf("feishu text api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary)
	}

	logger.DebugCF("feishu", "Feishu text message sent (fallback)", map[string]any{
		"chat_id": chatID,
	})

	if resp.Data != nil && resp.Data.MessageId != nil {
		return *resp.Data.MessageId, nil
	}
	return "", nil
}

// sendImage uploads an image and sends it as a message.
func (c *FeishuChannel) sendImage(ctx context.Context, chatID string, file *os.File) error {
	// Upload image to get image_key
	uploadReq := larkim.NewCreateImageReqBuilder().
		Body(larkim.NewCreateImageReqBodyBuilder().
			ImageType("message").

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Parse code=%d from the message: 99991663 -> fix/refresh app credentials; 230026 -> reword the message content; 99991400 -> slow down sends.
  2. Restore parity with the other paths: add c.invalidateTokenOnAuthError(resp.Code) before this return so a 99991663 self-heals on the next retry.
  3. Confirm receive_id is a chat_id (oc_...) and the bot is still in that chat.

Example fix

// before (feishu_64.go:1124) — no token invalidation, stale 99991663 never self-heals
if !resp.Success() {
    return "", fmt.Errorf("feishu text api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary)
}

// after — same recovery as the card path at line 1091
if !resp.Success() {
    c.invalidateTokenOnAuthError(resp.Code)
    return "", fmt.Errorf("feishu text api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary)
}
Defensive patterns

Strategy: retry

Validate before calling

if strings.TrimSpace(msg.Content) == "" {
    return nil // nothing to send as text fallback
}
if msg.ChatID == "" || !strings.HasPrefix(msg.ChatID, "oc_") {
    return fmt.Errorf("invalid feishu chat_id %q", msg.ChatID)
}

Type guard

func isTemporarySendErr(err error) bool {
    return errors.Is(err, channels.ErrTemporary)
}

Try / catch

if _, err := ch.Send(ctx, msg); err != nil {
    if errors.Is(err, channels.ErrTemporary) && strings.Contains(err.Error(), "code=99991663") {
        // stale token: this path does NOT auto-invalidate; fix credentials or restart channel to refresh cache
    }
}

Prevention

When it happens

Trigger: Card send fails, text fallback's Message.Create returns non-success: 99991663 stale tenant_access_token (not auto-invalidated on this path), 230026 content risk / blocked text, 99991400 rate limit, or invalid receive_id.

Common situations: Message content trips Feishu's content filter so both card and text fail; credentials rotated while the old token is still cached; replying to a chat the bot just left.

Related errors


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