sipeed/picoclaw · error · channels.ErrSendFailed
no media store available: %w
Error message
no media store available: %w
What it means
Thrown by MatrixChannel.SendMedia when c.GetMediaStore() returns nil (pkg/channels/matrix/matrix.go:474). The media store is the component that resolves part.Ref to a local file plus metadata (ResolveWithMeta); without it no media can be uploaded. Wrapping is channels.ErrSendFailed — permanent, no retry. The error means the channel instance was constructed without a media store wired in, i.e. a build/configuration defect, not a runtime Matrix failure.
Source
Thrown at pkg/channels/matrix/matrix.go:474
func (c *MatrixChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) ([]string, error) {
if !c.IsRunning() {
return nil, channels.ErrNotRunning
}
trackedMsgID, hasTrackedMsg := c.currentToolFeedbackMessage(msg.ChatID)
sendCtx := ctx
if sendCtx == nil {
sendCtx = context.Background()
}
roomID := id.RoomID(strings.TrimSpace(msg.ChatID))
if roomID == "" {
return nil, fmt.Errorf("matrix room ID is empty: %w", channels.ErrSendFailed)
}
store := c.GetMediaStore()
if store == nil {
return nil, fmt.Errorf("no media store available: %w", channels.ErrSendFailed)
}
var eventIDs []string
for _, part := range msg.Parts {
if err := sendCtx.Err(); err != nil {
return nil, err
}
localPath, meta, err := store.ResolveWithMeta(part.Ref)
if err != nil {
logger.ErrorCF("matrix", "Failed to resolve media ref", map[string]any{
"ref": part.Ref,
"error": err.Error(),
})
continue
}
fileInfo, err := os.Stat(localPath)View on GitHub (pinned to 49183d7e8d)
Solutions
- Find where MatrixChannel is built and wire the media store option/field before start
- Check capability instead of guessing: if GetMediaStore() is nil after construction, fail fast at startup rather than on first media send
- If media is intentionally unsupported, block media outbound messages upstream so SendMedia is never called
Example fix
// before ch := matrix.New(cfg, client) // no media store // after store := media.NewLocalStore(cfg.MediaDir) ch := matrix.New(cfg, client, matrix.WithMediaStore(store))
Defensive patterns
Strategy: validation
Validate before calling
// fail fast at construction/startup, not on first media send
if matrixCh.GetMediaStore() == nil {
return errors.New("media store not wired into matrix channel; media sending disabled")
} Try / catch
if _, err := matrixCh.SendMedia(ctx, msg); err != nil {
if errors.Is(err, channels.ErrSendFailed) && strings.Contains(err.Error(), "no media store") {
log.Error("channel built without media store; fix construction, message dead-lettered")
deadLetter(msg)
return nil
}
return err
} Prevention
- Wire the media store in the channel builder and assert non-nil at startup
- Disable media-producing features when the store is absent instead of failing per message
- Cover the media path in channel construction tests so unwired builds fail CI
When it happens
Trigger: Constructing MatrixChannel without the media-store option/dependency (framework wiring omitted); embedding the channel in a custom host app that never installs a media store; a nil store injected via empty constructor; calling SendMedia on a channel only intended for text.
Common situations: Custom integrations reusing MatrixChannel outside the standard bot bootstrap; refactors that made the media store optional and forgot one construction path; tests instantiating the channel minimally then exercising media sends.
Related errors
- matrix room ID is empty: %w
- empty matrix media URL
- invalid matrix media URL: %s
- create crypto helper: %w
- matrix upload media: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/a86cf887bf4f69b1.
Report an issue: GitHub.