sipeed/picoclaw · error · channels.ErrSendFailed

weixin send media: %w

Error message

weixin send media: %w

What it means

While iterating media parts of a Weixin outbound message, resolveOutboundPart failed to materialize one part's Ref into a local file, so the whole media send aborts, wrapping basechannels.ErrSendFailed. The failing ref is logged (chat_id, ref, error) before returning. Unlike the WeCom channel (error 665's sibling), this path does not fall back to a placeholder — the first unresolvable part stops the entire send.

Source

Thrown at pkg/channels/weixin/media.go:1128

		contextToken, _ = v.(string)
	}
	if contextToken == "" {
		return nil, fmt.Errorf(
			"weixin send media: missing context token for chat %s: %w",
			msg.ChatID,
			basechannels.ErrSendFailed,
		)
	}

	for _, part := range msg.Parts {
		localPath, filename, contentType, cleanup, err := c.resolveOutboundPart(ctx, part)
		if err != nil {
			logger.ErrorCF("weixin", "Failed to resolve outbound media", map[string]any{
				"chat_id": msg.ChatID,
				"ref":     part.Ref,
				"error":   err.Error(),
			})
			return nil, fmt.Errorf("weixin send media: %w", basechannels.ErrSendFailed)
		}
		func() {
			if cleanup != nil {
				defer cleanup()
			}

			kind := outboundMediaKind(part.Type, filename, contentType)
			uploaded, uploadErr := c.uploadLocalFile(ctx, localPath, filename, msg.ChatID, kind)
			if uploadErr != nil {
				err = uploadErr
				return
			}
			err = c.sendUploadedMedia(ctx, msg.ChatID, contextToken, part.Caption, kind, uploaded)
		}()
		if err != nil {
			logger.ErrorCF("weixin", "Failed to send outbound media", map[string]any{
				"chat_id": msg.ChatID,
				"ref":     part.Ref,

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Pre-validate each part's ref (store lookup or URL HEAD) before calling SendMedia Re-fetch or re-upload the referenced asset and retry the send Drop unresolvable parts and send the remainder plus caption text Fix storage retention: keep resolved files until the send completes (the cleanup func pattern implies files may be temp)

Example fix

// before
ch.SendMedia(ctx, msg) // contains a dead ref -> whole send aborts (error 678)

// after
parts := filterParts(msg.Parts, func(p MediaPart) bool {
    _, err := store.Stat(p.Ref)
    return err == nil
})
msg.Parts = parts
ch.SendMedia(ctx, msg)
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-resolve every part before invoking SendMedia
for _, part := range msg.Parts {
    if strings.TrimSpace(part.Ref) == "" {
        continue
    }
    if _, _, _, cleanup, err := ch.ResolveOutboundPart(ctx, part); err != nil {
        msg.Parts = dropPart(msg.Parts, part.Ref) // drop instead of failing the whole send
    } else if cleanup != nil {
        cleanup()
    }
}

Type guard

func isWeixinMediaResolveFail(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "weixin send media:") && errors.Is(err, basechannels.ErrSendFailed) && strings.Contains(err.Error(), "resolve")
}

Try / catch

if _, err := ch.SendMedia(ctx, msg); err != nil {
    if isWeixinMediaResolveFail(err) {
        // deterministic: filter bad refs, resend remaining parts (captions still go out)
        msg.Parts = filterParts(msg.Parts, alive)
        _, err = ch.SendMedia(ctx, msg)
    }
}

Prevention

When it happens

Trigger: Any part in msg.Parts whose Ref cannot be resolved: attachment store miss, expired/signed URL, unreachable storage, deleted cache file. The loop at media.go:1118 returns immediately on the first failure.

Common situations: Agent attaches a stale ref from an earlier turn whose backing file was evicted; media URL expired between generation and send; object store briefly down; local tmp cleaned by an aggressive janitor between sessions.

Related errors


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