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

  1. Find where MatrixChannel is built and wire the media store option/field before start
  2. Check capability instead of guessing: if GetMediaStore() is nil after construction, fail fast at startup rather than on first media send
  3. 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

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


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/a86cf887bf4f69b1. Report an issue: GitHub.