sipeed/picoclaw · error · ErrTemporary
feishu send media: %w
Error message
feishu send media: %w
What it means
A media part failed to upload/send (image or file) and the raw cause is REPLACED by the ErrTemporary sentinel: the underlying error is only logged (with part type), not wrapped into the returned error. Consequence: callers correctly see a retryable class, but must check logs for the real cause - and permanent 4xx causes are mislabeled as temporary.
Source
Thrown at pkg/channels/feishu/feishu_64.go:567
defer file.Close()
switch part.Type {
case "image":
err = c.sendImage(ctx, chatID, file)
default:
filename := part.Filename
if filename == "" {
filename = "file"
}
err = c.sendFile(ctx, chatID, file, filename, part.Type)
}
if err != nil {
logger.ErrorCF("feishu", "Failed to send media", map[string]any{
"type": part.Type,
"error": err.Error(),
})
return fmt.Errorf("feishu send media: %w", channels.ErrTemporary)
}
return nil
}
func firstMediaCaption(parts []bus.MediaPart) string {
for _, part := range parts {
if caption := strings.TrimSpace(part.Caption); caption != "" {
return caption
}
}
return ""
}
// --- Inbound message handling ---
func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
if event == nil || event.Event == nil || event.Event.Message == nil {
return nilView on GitHub (pinned to 49183d7e8d)
Solutions
- Check the channel logs first: 'Failed to send media' carries the true underlying error and part type
- Verify the media part is within Feishu limits and the media store is reachable
- Retry only when the logged cause is transient; a 4xx in logs will fail forever despite the temporary label
- Fix the wrapper to wrap rawErr (fmt.Errorf("feishu send media: %w: %w", channels.ErrTemporary, err)) so callers can classify
Example fix
// before (pkg/channels/feishu/feishu_64.go)
return fmt.Errorf("feishu send media: %w", channels.ErrTemporary) // cause lost
// after
return fmt.Errorf("feishu send media: %w: %w", channels.ErrTemporary, err) // class + cause Defensive patterns
Strategy: retry
Validate before calling
// cheap pre-checks before SendMedia
func mediaSendPreconditions(store MediaStore, msg bus.OutboundMediaMessage) error {
if store == nil { return errors.New("no media store") }
for _, p := range msg.Parts {
if p.Size > maxFeishuUploadBytes { return fmt.Errorf("part %q exceeds size limit", p.Filename) }
}
return nil
} Type guard
func isTemporarySendMedia(err error) bool {
return err != nil && errors.Is(err, channels.ErrTemporary)
} Try / catch
err := ch.SendMedia(ctx, msg)
for attempt := 0; isTemporarySendMedia(err) && attempt < 4; attempt++ {
time.Sleep(min(500*time.Millisecond<<attempt, 8*time.Second))
err = ch.SendMedia(ctx, msg)
}
// CAUTION: the sentinel hides the cause - if it keeps failing, read the channel log
// ('Failed to send media') and stop retrying if the cause is a 4xx/size error Prevention
- Check the channel logs for the true cause - the returned error deliberately drops it
- Pre-validate file size and store reachability to avoid mislabeled 'temporary' failures
- Cap retries: a permanent cause wearing ErrTemporary will fail every time
- Patch the wrapper to dual-wrap (%w: %w with rawErr) so callers can classify
When it happens
Trigger: sendImage/sendFile (via sendMediaPartFn) returns any error - oversized file rejected, media store fetch failure, upload timeout, unsupported part type - and the wrapper discards it in favor of ErrTemporary.
Common situations: Files over Feishu size limits; media store (S3/local) unreachable so the bytes never load; transient upload timeouts; a rare part type the switch does not handle falling into the error path.
Related errors
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/886f560a53e29be2.
Report an issue: GitHub.