sipeed/picoclaw · error

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

Error message

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

What it means

Feishu's image upload endpoint answered HTTP-level success semantics but with a non-zero business code (uploadResp.Success() false). The channel clears the cached tenant token when the code is 99991663. Typical codes: 230002 invalid image (unsupported format or >10MB), 99991663 invalid token, 99991400 rate limited.

Source

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

}

// 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").
			Image(file).
			Build()).
		Build()

	uploadResp, err := c.client.Im.V1.Image.Create(ctx, uploadReq)
	if err != nil {
		return fmt.Errorf("feishu image upload: %w", err)
	}
	if !uploadResp.Success() {
		c.invalidateTokenOnAuthError(uploadResp.Code)
		return fmt.Errorf("feishu image upload api error (code=%d msg=%s)", uploadResp.Code, uploadResp.Msg)
	}
	if uploadResp.Data == nil || uploadResp.Data.ImageKey == nil {
		return fmt.Errorf("feishu image upload: no image_key returned")
	}

	imageKey := *uploadResp.Data.ImageKey

	// 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()

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Match the code: 230002 -> convert to JPG/PNG and compress under 10MB; 99991663 -> fix credentials (cache is auto-cleared); 99991400 -> throttle uploads.
  2. Pre-validate size and MIME type before sending (see exampleFix).
  3. Grant im:image permission to the app in the Feishu console.
  4. If the file came from user upload, re-encode server-side instead of forwarding raw bytes.

Example fix

// before — any file goes straight to the API, 230002 surfaces after the upload
err := ch.SendMedia(ctx, msg)

// after — validate before uploading
if fi, serr := f.Stat(); serr == nil && fi.Size() > 10<<20 {
    return fmt.Errorf("image %s is %d bytes; Feishu limit is 10MB", name, fi.Size())
}
if mt := mime.TypeByExtension(filepath.Ext(name)); mt != "image/jpeg" && mt != "image/png" {
    return fmt.Errorf("image %s is %s; convert to JPEG/PNG first", name, mt)
}
Defensive patterns

Strategy: validation

Validate before calling

const feishuMaxImage = 10 << 20
if fi, serr := f.Stat(); serr != nil || fi.Size() > feishuMaxImage {
    return fmt.Errorf("image exceeds Feishu 10MB limit or is unreadable")
}
switch mime.TypeByExtension(filepath.Ext(name)) {
case "image/jpeg", "image/png":
default:
    return fmt.Errorf("image %s must be JPEG or PNG", name)
}

Type guard

func isFeishuUploadAPIErr(err error) bool {
    return strings.Contains(err.Error(), "feishu image upload api error (code=")
}

Try / catch

if err := ch.SendMedia(ctx, msg); err != nil {
    if strings.Contains(err.Error(), "code=230002") {
        // invalid image: convert/compress and retry — do not blind-retry the same bytes
    }
    if strings.Contains(err.Error(), "code=99991663") {
        // token: credentials problem, cache already cleared by the channel
    }
}

Prevention

When it happens

Trigger: Im.V1.Image.Create with ImageType('message') returns code 230002: file is not JPG/PNG, exceeds 10MB, or is corrupted; 99991663 after credential rotation; 99991400 from burst uploads; app missing im:image scope.

Common situations: Sending WebP/GIF/HEIC which message-type images reject; phone-sourced photos over 10MB; bot token regenerated in the developer console but still cached.

Related errors


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