{"record":{"id":"886f560a53e29be2","repo":"sipeed/picoclaw","slug":"feishu-send-media-w","errorCode":null,"errorMessage":"feishu send media: %w","messagePattern":"feishu send media: %w","errorType":"exception","errorClass":"ErrTemporary","httpStatus":null,"severity":"error","filePath":"pkg/channels/feishu/feishu_64.go","lineNumber":567,"sourceCode":"\tdefer file.Close()\n\n\tswitch part.Type {\n\tcase \"image\":\n\t\terr = c.sendImage(ctx, chatID, file)\n\tdefault:\n\t\tfilename := part.Filename\n\t\tif filename == \"\" {\n\t\t\tfilename = \"file\"\n\t\t}\n\t\terr = c.sendFile(ctx, chatID, file, filename, part.Type)\n\t}\n\n\tif err != nil {\n\t\tlogger.ErrorCF(\"feishu\", \"Failed to send media\", map[string]any{\n\t\t\t\"type\":  part.Type,\n\t\t\t\"error\": err.Error(),\n\t\t})\n\t\treturn fmt.Errorf(\"feishu send media: %w\", channels.ErrTemporary)\n\t}\n\treturn nil\n}\n\nfunc firstMediaCaption(parts []bus.MediaPart) string {\n\tfor _, part := range parts {\n\t\tif caption := strings.TrimSpace(part.Caption); caption != \"\" {\n\t\t\treturn caption\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// --- Inbound message handling ---\n\nfunc (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim.P2MessageReceiveV1) error {\n\tif event == nil || event.Event == nil || event.Event.Message == nil {\n\t\treturn nil","sourceCodeStart":549,"sourceCodeEnd":585,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/feishu/feishu_64.go#L549-L585","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (pkg/channels/feishu/feishu_64.go)\nreturn fmt.Errorf(\"feishu send media: %w\", channels.ErrTemporary) // cause lost\n\n// after\nreturn fmt.Errorf(\"feishu send media: %w: %w\", channels.ErrTemporary, err) // class + cause","handlingStrategy":"retry","validationCode":"// cheap pre-checks before SendMedia\nfunc mediaSendPreconditions(store MediaStore, msg bus.OutboundMediaMessage) error {\n\tif store == nil { return errors.New(\"no media store\") }\n\tfor _, p := range msg.Parts {\n\t\tif p.Size > maxFeishuUploadBytes { return fmt.Errorf(\"part %q exceeds size limit\", p.Filename) }\n\t}\n\treturn nil\n}","typeGuard":"func isTemporarySendMedia(err error) bool {\n\treturn err != nil && errors.Is(err, channels.ErrTemporary)\n}","tryCatchPattern":"err := ch.SendMedia(ctx, msg)\nfor attempt := 0; isTemporarySendMedia(err) && attempt < 4; attempt++ {\n\ttime.Sleep(min(500*time.Millisecond<<attempt, 8*time.Second))\n\terr = ch.SendMedia(ctx, msg)\n}\n// CAUTION: the sentinel hides the cause - if it keeps failing, read the channel log\n// ('Failed to send media') and stop retrying if the cause is a 4xx/size error","preventionTips":["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"],"tags":["feishu","media","upload","temporary","error-masking","retry"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}