multica-ai/multica · error
wecom: binding token invalid or expired
Error message
wecom: binding token invalid or expired
What it means
WeCom binding sentinel error: the token hash is unknown, already consumed, or expired (tokens live ~15 minutes; a CHECK enforces the TTL cap). One opaque error for all three sub-cases by design, to prevent a replay timing oracle — mirroring the slack/lark binding services.
Source
Thrown at server/internal/integrations/wecom/binding.go:71
// transport refused outright (no live connection mid-reconnect or lease flip)
// still suppresses the next minute of mints. Nothing here reacts to that
// error — the window itself, not a delivery receipt, is what bounds the
// damage, which is the other reason to keep it short.
//
// Sixty seconds does the job the throttle was written for: six lines typed in
// one breath still write one row, at a cost of at most one row a minute for a
// user who keeps going, against rows that expire in fifteen. The price of
// being wrong is one more message, not ten minutes of a bot insisting it
// already answered.
//
// It must stay comfortably inside BindingTokenTTL so a link a throttled user
// is pointed back at still has real time left on it.
const BindingTokenMintInterval = time.Minute
var (
// ErrBindingTokenInvalid: token unknown / already consumed / expired.
// One opaque error for all three avoids a replay timing oracle.
ErrBindingTokenInvalid = errors.New("wecom: binding token invalid or expired")
// ErrBindingAlreadyAssigned: this WeCom userid is already bound to a
// different Multica user (account transfer must go through explicit
// unbind, not implemented in iter 1 — an admin can DELETE the row).
ErrBindingAlreadyAssigned = errors.New("wecom: user id is already bound to a different user")
// ErrBindingNotWorkspaceMember: the redeemer is not a member of the
// token's workspace. Translated to 403 at the HTTP boundary.
ErrBindingNotWorkspaceMember = errors.New("wecom: redeemer is not a workspace member")
)
// BindingToken is a freshly minted token. The raw value is returned exactly
// once (embedded in the binding URL); only its hash is persisted.
type BindingToken struct {
Raw string
ExpiresAt time.Time
// Reused says the throttle suppressed the mint because a live link is
// already sitting in the user's chat. Raw is empty in that case and there
// is no way to recover it — the table only ever held the hash — so theView on GitHub (pinned to 2c0912b6ec)
Solutions
- Reply to the WeCom bot after the 1-minute mint interval has passed to get a genuinely new link, and redeem it promptly.
- If the throttle reused the existing link (Reused=true), scroll back to the earlier bot message and use that link while it still has time left.
- Ensure the URL is copied whole — truncation makes the hash unknown and yields the same opaque error.
Example fix
// before
res, err := svc.Redeem(ctx, rawToken)
if err != nil {
panic(err)
}
// after
res, err := svc.Redeem(ctx, rawToken)
if errors.Is(err, wecom.ErrBindingTokenInvalid) {
// unknown / consumed / expired are indistinguishable by design
renderLinkExpired(w) // "link invalid or expired, ask the bot for a new one"
return
} Defensive patterns
Strategy: try-catch
Try / catch
res, err := wecomSvc.Redeem(ctx, rawToken)
if err != nil {
if errors.Is(err, wecom.ErrBindingTokenInvalid) {
// unknown/consumed/expired are collapsed by design — no branching on sub-case
return renderLinkExpired(w)
}
return err
} Prevention
- Redeem the WeCom link immediately; the 1-minute mint throttle means asking again too soon returns the same link.
- Copy the full URL from the bot message; truncated tokens hash to unknown values.
- Handle Reused=true mint results by pointing the user at the earlier message, not by minting.
When it happens
Trigger: Redeeming a WeCom binding token a second time (single-use: only the hash is stored and redemption consumes it), redeeming after the 15-minute TTL, or redeeming a mistyped/truncated token from the chat message. Note the mint throttle (BindingTokenMintInterval = 1 minute) can hand back a REUSED live link whose raw token is unrecoverable — pointing the user at the earlier message is the only path.
Common situations: User double-clicks the WeCom bot's binding link; user delays past the TTL; WeCom client renders the URL with ellipsis/truncation; user re-requests a link within the 1-minute throttle and gets the old one back, then mis-copies it.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- slack: binding token invalid or expired
- wecom: user id is already bound to a different user
- lark open_id is already bound to a different user
- slack: user id is already bound to a different user
- wecom: redeemer is not a workspace member
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/5f45a0adb4b6297a.
Report an issue: GitHub.