sipeed/picoclaw · error

feishu image send api error (code=%d msg=%s)

Error message

feishu image send api error (code=%d msg=%s)

What it means

Feishu rejected the image message send with a non-zero business code after the upload succeeded. invalidateTokenOnAuthError runs first, so a 99991663 clears the token cache. The dominant code is 230002 invalid image_key — keys are app-scoped and short-lived.

Source

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

	// Send image message
	content, _ := json.Marshal(map[string]string{"image_key": imageKey})
	req := larkim.NewCreateMessageReqBuilder().
		ReceiveIdType(larkim.CreateMessageV1ReceiveIDTypeChatId).
		Body(larkim.NewCreateMessageReqBodyBuilder().
			ReceiveId(chatID).
			MsgType(larkim.MsgTypeImage).
			Content(string(content)).
			Build()).
		Build()

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

// sendFile uploads a file and sends it as a message.
func (c *FeishuChannel) sendFile(ctx context.Context, chatID string, file *os.File, filename, fileType string) error {
	// Map part type to Feishu file type
	feishuFileType := "stream"
	switch fileType {
	case "audio":
		feishuFileType = "opus"
	case "video":
		feishuFileType = "mp4"
	}

	// Upload file to get file_key
	uploadReq := larkim.NewCreateFileReqBuilder().
		Body(larkim.NewCreateFileReqBodyBuilder().

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Treat upload+send as one unit: on this error, re-upload to get a fresh image_key and immediately send.
  2. Confirm upload and send use the same app credentials (image_key is bound to the app).
  3. Verify the bot is still in the target chat and has im:message scope.
  4. If code is 99991663, the token cache was already cleared — the next attempt fetches a fresh token.

Example fix

// before — upload once, send; a backoff-delayed retry can outlive the image_key
key := upload(ctx, f)
err := sendImageMsg(ctx, chatID, key)

// after — re-upload on invalid-key so retry always uses a fresh key
key := upload(ctx, f)
err := sendImageMsg(ctx, chatID, key)
if err != nil && strings.Contains(err.Error(), "code=230002") {
    key = upload(ctx, f)
    err = sendImageMsg(ctx, chatID, key)
}
Defensive patterns

Strategy: retry

Validate before calling

// minimize key age: upload immediately before send
// (there is no API to validate an image_key without spending it)

Type guard

func isInvalidImageKey(err error) bool {
    return strings.Contains(err.Error(), "feishu image send api error") && strings.Contains(err.Error(), "code=230002")
}

Try / catch

if err := sendImageFlow(ctx, chatID, f); err != nil {
    if isInvalidImageKey(err) {
        // key expired/app-mismatch: re-upload and resend immediately
        err = sendImageFlow(ctx, chatID, f)
    }
    return err
}

Prevention

When it happens

Trigger: Message.Create with msg_type=image fails: image_key expired because too much time passed between upload and send; key was minted by a different app_id; bot lacks permission to the target chat; 99991663 stale token.

Common situations: Retries after backoff that outlive the key's validity; uploading with one credential and sending with another after a config change; bot removed from the chat between upload and send.

Related errors


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