sipeed/picoclaw · error · channels.ErrSendFailed
weixin send media: missing context token for chat %s: %w
Error message
weixin send media: missing context token for chat %s: %w
What it means
The media send path of the Weixin channel refuses to send because no context_token is stored for msg.ChatID — the same gating as error 674 but for media parts. The iLink media upload/send calls need the token captured from the user's last inbound message; without it the channel fails fast (wrapping basechannels.ErrSendFailed) before resolving or uploading any files.
Source
Thrown at pkg/channels/weixin/media.go:1113
return stop, nil
}
// SendMedia implements channels.MediaSender.
func (c *WeixinChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) ([]string, error) {
if !c.IsRunning() {
return nil, basechannels.ErrNotRunning
}
if err := c.ensureSessionActive(); err != nil {
return nil, err
}
contextToken := ""
if v, ok := c.contextTokens.Load(msg.ChatID); ok {
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 {View on GitHub (pinned to 49183d7e8d)
Solutions
- Ensure an inbound message from the chat exists before sending media (reply-only semantics) After restarts, have users send any message to re-arm the token, then resend the media Verify msg.ChatID equals the inbound user id exactly (no mapping drift) Send the payload as text/URL if the user cannot be asked to re-engage
Example fix
// before
ch.SendMedia(ctx, msg) // msg.ChatID never seen inbound -> error 677
// after
if _, ok := ch.ContextToken(msg.ChatID); !ok {
return skipMediaReply(msg.ChatID)
}
ch.SendMedia(ctx, msg) Defensive patterns
Strategy: validation
Validate before calling
// gate media sends on token availability (export ContextToken)
if _, ok := ch.ContextToken(msg.ChatID); !ok {
return skipMedia(msg.ChatID) // nothing to send without an inbound history
}
return ch.SendMedia(ctx, msg) Type guard
func isWeixinMediaMissingToken(err error) bool {
return err != nil && strings.Contains(err.Error(), "weixin send media: missing context token")
} Try / catch
if _, err := ch.SendMedia(ctx, msg); err != nil {
if isWeixinMediaMissingToken(err) {
// non-retryable: stash the media; deliver on the user's next inbound
parkUntilInbound(msg)
}
} Prevention
- Treat media replies like text replies: inbound-first, always
- Re-check token presence after channel restarts before flushing queued media
- Use the identical ChatID string captured from inbound for sends
- Offer a text/URL fallback when media cannot legally be delivered
When it happens
Trigger: Sending media to a chat that has not messaged the bot since channel start; after a restart (tokens are in-memory, c.contextTokens is cleared); using a ChatID that differs from the key stored on inbound.
Common situations: Bot tries to proactively push an image/file to a user who never wrote; restart wiped tokens so media replies to old threads fail until the user pings again; media reply routed through a rewritten chat id.
Related errors
- weixin send: %w: missing context token for chat %s
- weixin send media: %w
- no media store available: %w
- no media store available: %w
- wecom resolve media %q: %v: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/faebf40ffbfe4033.
Report an issue: GitHub.