sipeed/picoclaw · error

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

Error message

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

What it means

Feishu's file upload endpoint returned a non-zero business code (uploadResp.Success() false). invalidateTokenOnAuthError runs first, clearing the token cache on 99991663. Typical codes: 230002 invalid file (size >30MB, unsupported/mismatched file_type), 99991663 token, 99991400 rate limit.

Source

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

		feishuFileType = "mp4"
	}

	// Upload file to get file_key
	uploadReq := larkim.NewCreateFileReqBuilder().
		Body(larkim.NewCreateFileReqBodyBuilder().
			FileType(feishuFileType).
			FileName(filename).
			File(file).
			Build()).
		Build()

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

	fileKey := *uploadResp.Data.FileKey

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

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Match the code: 230002 -> check 30MB limit and that file_type matches the actual content (audio=opus, video=mp4, else stream); 99991663 -> fix credentials; 99991400 -> throttle.
  2. Pre-validate size and extension-to-type mapping before upload (see exampleFix).
  3. Grant im:file permission to the app.
  4. Re-encode oversized media instead of forwarding raw.

Example fix

// before
err := ch.SendMedia(ctx, msg) // 230002 after full upload of an oversized file

// after — validate before uploading
if fi, serr := f.Stat(); serr == nil && fi.Size() > 30<<20 {
    return fmt.Errorf("file %s exceeds Feishu 30MB limit (%d bytes)", name, fi.Size())
}
wantType := map[string]string{"audio": "opus", "video": "mp4"}[fileType]
if wantType == "" { wantType = "stream" } // must match the value used in CreateFileReqBodyBuilder
Defensive patterns

Strategy: validation

Validate before calling

const feishuMaxFile = 30 << 20
if fi, serr := f.Stat(); serr != nil || fi.Size() > feishuMaxFile {
    return fmt.Errorf("file exceeds Feishu 30MB limit or is unreadable")
}
want := map[string]string{"audio": "opus", "video": "mp4"}[fileType]
if want == "" { want = "stream" }
// ensure the same 'want' is what the channel passes as feishuFileType

Type guard

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

Try / catch

if err := ch.SendMedia(ctx, msg); err != nil {
    if strings.Contains(err.Error(), "code=230002") {
        // invalid file/type: fix size, encoding, or type mapping — retrying same bytes fails again
    }
    if strings.Contains(err.Error(), "code=99991663") {
        // token issue; cache already invalidated
    }
}

Prevention

When it happens

Trigger: Im.V1.File.Create with FileType('stream'|'opus'|'mp4') fails: file exceeds 30MB; audio/video sent with the wrong feishuFileType mapping; 99991663 stale token; app lacks im:file scope; burst uploads rate-limited.

Common situations: Forwarding large media from other channels; sending a .ogg recorded as opus but declared stream; credentials rotated while old token cached.

Related errors


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