sipeed/picoclaw · error

feishu edit api error (code=%d msg=%s)

Error message

feishu edit api error (code=%d msg=%s)

What it means

FeishuChannel.EditMessage's Patch call completed and the response parsed, but resp.Success() is false: Feishu returned a non-zero business code. Common codes: 99991661/99991663 (invalid or expired tenant_access_token), 230002 (no permission / bot not in chat), invalid message_id, or the message is not editable (e.g. it was a plain-text fallback message, which cannot be card-patched). invalidateTokenOnAuthError(resp.Code) already ran for auth-class codes.

Source

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

// Uses Message.Patch to update an interactive card message.
func (c *FeishuChannel) EditMessage(ctx context.Context, chatID, messageID, content string) error {
	cardContent, err := buildMarkdownCard(content)
	if err != nil {
		return fmt.Errorf("feishu edit: card build failed: %w", err)
	}

	req := larkim.NewPatchMessageReqBuilder().
		MessageId(messageID).
		Body(larkim.NewPatchMessageReqBodyBuilder().Content(cardContent).Build()).
		Build()

	resp, err := c.client.Im.V1.Message.Patch(ctx, req)
	if err != nil {
		return fmt.Errorf("feishu edit: %w", err)
	}
	if !resp.Success() {
		c.invalidateTokenOnAuthError(resp.Code)
		return fmt.Errorf("feishu edit api error (code=%d msg=%s)", resp.Code, resp.Msg)
	}
	return nil
}

// DeleteMessage implements channels.MessageDeleter.
func (c *FeishuChannel) DeleteMessage(ctx context.Context, chatID, messageID string) error {
	deleteFn := c.deleteMessageFn
	if deleteFn == nil {
		deleteFn = c.deleteMessageAPI
	}
	return deleteFn(ctx, chatID, messageID)
}

func (c *FeishuChannel) deleteMessageAPI(ctx context.Context, chatID, messageID string) error {
	req := larkim.NewDeleteMessageReqBuilder().
		MessageId(messageID).
		Build()

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Read the code in the message: 99991661/99991663 means auth - let the token refresh (invalidateTokenOnAuthError already triggered) or recheck app credentials
  2. 230002/permission codes: confirm the bot is still in the chat and has im:message scope
  3. Invalid message_id or non-editable message: stop patching and delete + resend a fresh message instead
  4. Do not blind-retry permanent business errors; surface them to the pipeline

Example fix

// before
if err := ch.EditMessage(ctx, chatID, msgID, content); err != nil {
	retry() // wrong: business codes are usually permanent
}

// after
err := ch.EditMessage(ctx, chatID, msgID, content)
if code, ok := feishuAPICode(err); ok {
	switch code {
	case 99991661, 99991663: // auth: token already invalidated, next call re-auths
		retrySoon()
	default:
		resendAsNewMessage(ctx, chatID, content) // patch impossible
	}
}
Defensive patterns

Strategy: try-catch

Validate before calling

null

Type guard

var feishuCodeRe = regexp.MustCompile(`api error \(code=(\d+)`)

func feishuErrorCode(err error) (int, bool) {
	if err == nil { return 0, false }
	m := feishuCodeRe.FindStringSubmatch(err.Error())
	if m == nil { return 0, false }
	n, _ := strconv.Atoi(m[1])
	return n, true
}

Try / catch

if err := ch.EditMessage(ctx, chatID, msgID, content); err != nil {
	if code, ok := feishuErrorCode(err); ok {
		switch code {
		case 99991661, 99991663: // auth: token invalidation already triggered
			time.Sleep(time.Second); retryOnce()
		default: // permission / bad message_id: permanent
			resendAsNewMessage(ctx, chatID, content)
		}
	}
}

Prevention

When it happens

Trigger: Message.Patch responds with code!=0: editing a deleted/nonexistent message_id, editing a message the bot lacks permission for, expired tenant token, or patching a non-card message type.

Common situations: Editing a placeholder that already fell back to plain text; bot removed from the chat between send and edit; token invalidated by concurrent restarts or credential rotation; message_id from a different tenant/app.

Related errors


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