{"record":{"id":"c502ec2a5b3a5c4e","repo":"chenhg5/cc-connect","slug":"weixin-context-token-is-required-for-send","errorCode":null,"errorMessage":"weixin: context_token is required for send","messagePattern":"weixin: context_token is required for send","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/weixin/client.go","lineNumber":255,"sourceCode":"\nfunc (c *apiClient) sendTyping(ctx context.Context, userID, typingTicket string, status int) error {\n\treq := sendTypingReq{\n\t\tIlinkUserID:  userID,\n\t\tTypingTicket: typingTicket,\n\t\tStatus:       status,\n\t\tBaseInfo:     baseInfo{ChannelVersion: channelVersion},\n\t}\n\tpayload, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = c.post(ctx, \"ilink/bot/sendtyping\", payload, 0, \"sendTyping\")\n\treturn err\n}\n\nfunc (c *apiClient) sendText(ctx context.Context, to, text, contextToken, clientID string) error {\n\tif strings.TrimSpace(contextToken) == \"\" {\n\t\treturn fmt.Errorf(\"weixin: context_token is required for send\")\n\t}\n\titems := []messageItem{}\n\tif strings.TrimSpace(text) != \"\" {\n\t\titems = append(items, messageItem{\n\t\t\tType:     messageItemText,\n\t\t\tTextItem: &textItem{Text: text},\n\t\t})\n\t}\n\tif len(items) == 0 {\n\t\treturn fmt.Errorf(\"weixin: sendText: empty item_list\")\n\t}\n\tmsg := sendMessageReq{\n\t\tMsg: weixinOutboundMsg{\n\t\t\tFromUserID:   \"\",\n\t\t\tToUserID:     to,\n\t\t\tClientID:     clientID,\n\t\t\tMessageType:  messageTypeBot,\n\t\t\tMessageState: messageStateFinish,","sourceCodeStart":237,"sourceCodeEnd":273,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/weixin/client.go#L237-L273","documentation":"WeChat iLink outbound sends require a context_token obtained from the conversation config; the client enforces this before building the request. sendText refuses to send when the token is empty because the API would reject the message anyway.","triggerScenarios":"sendText invoked with an empty contextToken — typically when the getConfig step was skipped, failed silently, or the token expired and was cleared before this send.","commonSituations":"Bot restart losing cached per-user context tokens; getConfig previously failed (errors 1716/1717) and the caller proceeded anyway; first message to a user before config was ever fetched.","solutions":["Ensure getConfig runs and its result is cached per user before any send","Fail earlier: propagate the getConfig error instead of continuing with an empty token","Re-fetch config on this error and retry the send once","Verify session persistence so tokens survive restarts"],"exampleFix":"// before\nif err := getConfig(...); err != nil { slog.Warn(\"...\"); }\nc.sendText(ctx, to, text, token, id) // token may be empty\n// after\nif err := getConfig(...); err != nil { return fmt.Errorf(\"weixin: get config: %w\", err) }\nc.sendText(ctx, to, text, token, id)","handlingStrategy":"validation","validationCode":"if strings.TrimSpace(contextToken) == \"\" { return errors.New(\"cannot send: context_token missing; fetch config first\") }","typeGuard":"func hasContextToken(t string) bool { return strings.TrimSpace(t) != \"\" }","tryCatchPattern":"if err := sendText(ctx, to, text, token, id); err != nil {\n\tif strings.Contains(err.Error(), \"context_token is required\") {\n\t\tif tok, cerr := fetchContextToken(ctx, to); cerr == nil { return sendText(ctx, to, text, tok, id) }\n\t}\n\treturn err\n}","preventionTips":["Always cache context tokens per user after getConfig","Never swallow getConfig errors in the send path","Persist tokens so restarts don't lose them","Re-fetch the token on this error and retry once"],"tags":["weixin","validation","context-token","send"],"backgroundTag":"empty-required-field","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}