sipeed/picoclaw · error
read media file: %w
Error message
read media file: %w
What it means
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).
Source
Thrown at pkg/channels/wecom/media.go:682
if err := json.Unmarshal(env.Body, &out); err != nil {
return out, fmt.Errorf("decode wecom response body: %w", err)
}
return out, nil
}
func (c *WeComChannel) uploadOutboundMedia(
ctx context.Context,
localPath, filename, contentType string,
part bus.MediaPart,
) (*wecomOutboundMedia, error) {
_ = ctx
contentType = detectLocalWeComContentType(localPath, contentType)
filename = ensureWeComOutboundFilename(filename, localPath, contentType)
data, err := os.ReadFile(localPath)
if err != nil {
return nil, fmt.Errorf("read media file: %w", err)
}
size := int64(len(data))
kind := outboundWeComMediaKind(part.Type, filename, contentType, size)
if kind == "" {
return nil, fmt.Errorf("unsupported wecom media type or size for %q", filename)
}
totalChunks := (len(data) + wecomUploadChunkMaxBytes - 1) / wecomUploadChunkMaxBytes
if totalChunks <= 0 || totalChunks > wecomUploadMaxChunks {
return nil, fmt.Errorf("wecom upload requires 1-%d chunks, got %d", wecomUploadMaxChunks, totalChunks)
}
sum := md5.Sum(data)
initEnv, err := c.sendCommandAck(wecomCommand{
Cmd: wecomCmdUploadMediaInit,
Headers: wecomHeaders{ReqID: randomID(10)},
Body: wecomUploadMediaInitBody{
Type: kind,View on GitHub (pinned to 49183d7e8d)
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
Example fix
// before: two goroutines upload the same media:// path concurrently
localPath, _, _, cleanup, _ := c.resolveOutboundPart(ctx, part)
defer cleanup() // first finisher deletes the shared file
// after: private copy per sender before upload
src, err := os.Open(localPath)
if err != nil { return err }
defer src.Close()
priv, err := os.CreateTemp("", "send-*")
if err != nil { return err }
io.Copy(priv, src)
priv.Close()
defer os.Remove(priv.Name()) // upload reads priv.Name() Defensive patterns
Strategy: validation
Validate before calling
// re-validate just before the read, and fail loudly on races
func fileReadableForUpload(path string, maxBytes int64) error {
fi, err := os.Stat(path)
if err != nil {
return fmt.Errorf("media vanished before upload: %w", err)
}
if !fi.Mode().IsRegular() {
return fmt.Errorf("%s is not a regular file", path)
}
if fi.Size() > maxBytes {
return fmt.Errorf("%s grew to %d bytes", path, fi.Size())
}
return nil
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Fan-out bots forwarding one stored attachment to several chats; agents retrying sends while cleanup runs; users passing /tmp paths that another process rotates.
Related errors
- invalid pid file
- get QR channel: %w
- channel stopped during QR setup
- credential: failed to read credential file %q: %w
- events: bus is closed
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/01055627135e8cd3.
Report an issue: GitHub.