{"record":{"id":"a0f3c5aebca6682a","repo":"chenhg5/cc-connect","slug":"wecom-send-failed-d-s","errorCode":null,"errorMessage":"wecom: send failed: %d %s","messagePattern":"wecom: send failed: (.+?) (.+?)","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/wecom/wecom.go","lineNumber":652,"sourceCode":"\tapiURL := p.wecomAPIURL(\"/cgi-bin/message/send\", url.Values{\n\t\t\"access_token\": []string{accessToken},\n\t})\n\n\tresp, err := p.apiClient.Post(apiURL, \"application/json\", strings.NewReader(string(body)))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wecom: send message: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar result struct {\n\t\tErrCode int    `json:\"errcode\"`\n\t\tErrMsg  string `json:\"errmsg\"`\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn fmt.Errorf(\"wecom: decode send response: %w\", err)\n\t}\n\tif result.ErrCode != 0 {\n\t\treturn fmt.Errorf(\"wecom: send failed: %d %s\", result.ErrCode, result.ErrMsg)\n\t}\n\treturn nil\n}\n\nfunc (p *Platform) getAccessToken() (string, error) {\n\tp.tokenCache.mu.Lock()\n\tdefer p.tokenCache.mu.Unlock()\n\n\tif p.tokenCache.token != \"\" && time.Now().Before(p.tokenCache.expiresAt) {\n\t\treturn p.tokenCache.token, nil\n\t}\n\n\tapiURL := p.wecomAPIURL(\"/cgi-bin/gettoken\", url.Values{\n\t\t\"corpid\":     []string{p.corpID},\n\t\t\"corpsecret\": []string{p.corpSecret},\n\t})\n\n\tresp, err := p.apiClient.Get(apiURL)","sourceCodeStart":634,"sourceCodeEnd":670,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/wecom/wecom.go#L634-L670","documentation":"This error is returned by sendText when the WeCom /cgi-bin/message/send API responded successfully at the HTTP level but reported a business failure via a non-zero errcode in its JSON response. The message carries the numeric errcode and server errmsg, e.g. 'wecom: send failed: 40058 invalid parameter'. Matching the errcode against WeCom's documented error table is the required first step for diagnosis.","triggerScenarios":"sendText posts a payload of the form {\"touser\":..., \"msgtype\":\"text\", \"agentid\":..., \"text\":{\"content\":...}} and the response contains result.ErrCode != 0. Frequent codes: 40014/42001 invalid/expired access_token; 81013 unknown touser; 40058 bad agentid or parameter format; 45009 rate limit; 81004 disabled app.","commonSituations":"1) stale cached access_token after a corpsecret rotation; 2) agentid mismatch between config.toml and the WeCom admin console; 3) target userid not in the app's visible scope or deleted; 4) empty/oversized text content; 5) exceeding API rate limits when the bot fans out to many chats.","solutions":["Look up the printed errcode in WeCom's error-code documentation to identify the exact business cause.","For 40014/42001, invalidate the cached token (getAccessToken's tokenCache) and verify corpid/corpsecret; the next getAccessToken call will mint a fresh token.","For 81013, validate the touser list: check userids exist and are within the app's visible range.","For 40058, confirm agentid in config.toml matches the WeCom app and that the text payload shape is correct.","For 45009, throttle sends with backoff; batch or queue messages instead of firing them concurrently."],"exampleFix":"// before\nif result.ErrCode != 0 {\n    return fmt.Errorf(\"wecom: send failed: %d %s\", result.ErrCode, result.ErrMsg)\n}\n// after — auto-recover on token expiry\nif result.ErrCode == 40014 || result.ErrCode == 42001 {\n    p.tokenCache.mu.Lock()\n    p.tokenCache.token = \"\"\n    p.tokenCache.expiry = time.Time{}\n    p.tokenCache.mu.Unlock()\n    return fmt.Errorf(\"wecom: send failed (token expired, cache cleared, will retry): %d %s\", result.ErrCode, result.ErrMsg)\n}\nif result.ErrCode != 0 {\n    return fmt.Errorf(\"wecom: send failed: %d %s\", result.ErrCode, result.ErrMsg)\n}","handlingStrategy":"try-catch","validationCode":"// Pre-flight checks before sendText:\nif accessToken == \"\" || agentID == 0 || toUser == \"\" || strings.TrimSpace(text) == \"\" {\n    return fmt.Errorf(\"wecom: pre-send validation failed: missing token/agentid/user/content\")\n}\nif len([]rune(text)) > 2048 { // WeCom text content practical limit\n    return fmt.Errorf(\"wecom: text content too long: %d runes\", len([]rune(text)))\n}","typeGuard":"func wecomSendErrCode(err error) (int, bool) {\n    if err == nil {\n        return 0, false\n    }\n    var code int\n    if _, scanErr := fmt.Sscanf(err.Error(), \"wecom: send failed: %d\", &code); scanErr == nil {\n        return code, true\n    }\n    return 0, false\n}","tryCatchPattern":"if err := p.Send(msg); err != nil {\n    if code, ok := wecomSendErrCode(err); ok {\n        switch code {\n        case 40014, 42001: // token expired: clear cache and retry once\n            p.invalidateTokenCache()\n        case 81013: // bad touser: fix recipient config\n            slog.Warn(\"wecom: unknown touser\", \"err\", err)\n        case 45009: // rate limited: back off\n            time.Sleep(rateBackoff)\n        default:\n            slog.Error(\"wecom: send text rejected\", \"errcode\", code, \"errmsg\", err)\n        }\n    }\n}","preventionTips":["Parse and route on errcode: auth codes (40014/42001), recipient codes (81013), and rate-limit codes (45009) need different handling.","Verify agentid, corpid, and corpsecret in config.toml against the WeCom admin console at startup (doctor check).","Auto-refresh the token cache when an auth errcode appears instead of requiring a restart.","Validate target userids and keep text payloads within WeCom size limits; queue fan-out sends to respect rate limits."],"tags":["wecom","api","errcode","text-message"],"backgroundTag":"api-error-response","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"}