sipeed/picoclaw · error

feishu image send: %w

Error message

feishu image send: %w

What it means

After a successful upload, the follow-up Message.Create that sends the image_key failed at transport level (network error, timeout, closed connection). The underlying error is preserved with %w. The upload itself already consumed the image, so a retry must re-send the message only while the image_key is still valid.

Source

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

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

	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"
	}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Unwrap the cause (url.Error/net.OpError) to confirm transport vs context cancellation.
  2. Retry the send step; if the retry reports an invalid image_key (see 487), redo upload+send as one unit.
  3. Bound the whole media flow (upload+send) with one generous ctx deadline instead of two tight ones.

Example fix

// before
err := ch.SendMedia(ctx, msg)

// after
mediaCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
err := ch.SendMedia(mediaCtx, msg)
if errors.Is(err, context.DeadlineExceeded) {
    // retry the whole upload+send flow with a fresh key
}
Defensive patterns

Strategy: retry

Validate before calling

mediaCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
// pass mediaCtx so upload+send share one generous deadline

Type guard

func isTimeout(err error) bool {
    var urlErr *url.Error
    return errors.As(err, &urlErr) && urlErr.Timeout()
}

Try / catch

if err := ch.SendMedia(ctx, msg); err != nil {
    if isTimeout(err) || errors.Is(err, context.DeadlineExceeded) {
        // retry the whole flow — but expect the image_key may now be expired (code 230002 on next send)
    }
}

Prevention

When it happens

Trigger: Connection drop or client timeout between upload and send; the ctx cancelled mid-request; SDK error building the msg_type=image request.

Common situations: Long GC pause or pod eviction between upload and send; mobile/spot network; ctx deadline set too tight for the two-step upload+send flow.

Related errors


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