{"record":{"id":"01055627135e8cd3","repo":"sipeed/picoclaw","slug":"read-media-file-w","errorCode":null,"errorMessage":"read media file: %w","messagePattern":"read media file: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/channels/wecom/media.go","lineNumber":682,"sourceCode":"\tif err := json.Unmarshal(env.Body, &out); err != nil {\n\t\treturn out, fmt.Errorf(\"decode wecom response body: %w\", err)\n\t}\n\treturn out, nil\n}\n\nfunc (c *WeComChannel) uploadOutboundMedia(\n\tctx context.Context,\n\tlocalPath, filename, contentType string,\n\tpart bus.MediaPart,\n) (*wecomOutboundMedia, error) {\n\t_ = ctx\n\n\tcontentType = detectLocalWeComContentType(localPath, contentType)\n\tfilename = ensureWeComOutboundFilename(filename, localPath, contentType)\n\n\tdata, err := os.ReadFile(localPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read media file: %w\", err)\n\t}\n\tsize := int64(len(data))\n\tkind := outboundWeComMediaKind(part.Type, filename, contentType, size)\n\tif kind == \"\" {\n\t\treturn nil, fmt.Errorf(\"unsupported wecom media type or size for %q\", filename)\n\t}\n\n\ttotalChunks := (len(data) + wecomUploadChunkMaxBytes - 1) / wecomUploadChunkMaxBytes\n\tif totalChunks <= 0 || totalChunks > wecomUploadMaxChunks {\n\t\treturn nil, fmt.Errorf(\"wecom upload requires 1-%d chunks, got %d\", wecomUploadMaxChunks, totalChunks)\n\t}\n\n\tsum := md5.Sum(data)\n\tinitEnv, err := c.sendCommandAck(wecomCommand{\n\t\tCmd:     wecomCmdUploadMediaInit,\n\t\tHeaders: wecomHeaders{ReqID: randomID(10)},\n\t\tBody: wecomUploadMediaInitBody{\n\t\t\tType:        kind,","sourceCodeStart":664,"sourceCodeEnd":700,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/wecom/media.go#L664-L700","documentation":"os.ReadFile(localPath) failed at the start of uploadOutboundMedia (media.go:680-682). localPath was just produced by resolveOutboundPart, so the file existed moments earlier - the usual causes are lifecycle races: a media:// ref stored with CleanupPolicyDeleteOnCleanup deleted by a concurrent send that finished first, a file:// path removed between the os.Stat in resolveOutboundPart and this read, or permission changes. The %w keeps the *fs.PathError (ENOENT/EACCES).","triggerScenarios":"Two sends reference the same media:// ref concurrently: the first completes, its cleanup deletes the backing file, the second's ReadFile gets ENOENT; or a file:// part points at a transient file that vanished; or store GC purged the object between resolve and upload.","commonSituations":"Fan-out bots forwarding one stored attachment to several chats; agents retrying sends while cleanup runs; users passing /tmp paths that another process rotates.","solutions":["Check errors.Is(err, fs.ErrNotExist) - a missing file right after resolve is a race, not corruption","Avoid concurrent sends of the same media:// ref, or copy it to a private temp file per send before uploading","Ensure cleanup runs only after the last consumer finishes (defer the cleanup func until after uploadOutboundMedia, as wecom.go:243-253 does)","For file:// refs, use stable paths outside /tmp or re-store the file in the media store"],"exampleFix":"// before: two goroutines upload the same media:// path concurrently\nlocalPath, _, _, cleanup, _ := c.resolveOutboundPart(ctx, part)\ndefer cleanup() // first finisher deletes the shared file\n\n// after: private copy per sender before upload\nsrc, err := os.Open(localPath)\nif err != nil { return err }\ndefer src.Close()\npriv, err := os.CreateTemp(\"\", \"send-*\")\nif err != nil { return err }\nio.Copy(priv, src)\npriv.Close()\ndefer os.Remove(priv.Name()) // upload reads priv.Name()","handlingStrategy":"validation","validationCode":"// re-validate just before the read, and fail loudly on races\nfunc fileReadableForUpload(path string, maxBytes int64) error {\n    fi, err := os.Stat(path)\n    if err != nil {\n        return fmt.Errorf(\"media vanished before upload: %w\", err)\n    }\n    if !fi.Mode().IsRegular() {\n        return fmt.Errorf(\"%s is not a regular file\", path)\n    }\n    if fi.Size() > maxBytes {\n        return fmt.Errorf(\"%s grew to %d bytes\", path, fi.Size())\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["one media:// ref, one in-flight send - serialize or copy-per-send","run the cleanup func only after the upload returns, never before","prefer the media store over raw /tmp paths for anything a send references"],"tags":["filesystem","race","lifecycle","media-store"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}