{"record":{"id":"548e5b4e5af3b2e8","repo":"Tencent/WeKnora","slug":"get-token-error-code-d-msg-s","errorCode":null,"errorMessage":"get token error: code=%d msg=%s","messagePattern":"get token error: code=(.+?) msg=(.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/im/wecom/webhook_adapter.go","lineNumber":420,"sourceCode":"\t}\n\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"request access token: %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\tAccessToken string `json:\"access_token\"`\n\t\tExpiresIn   int    `json:\"expires_in\"` // seconds\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn \"\", fmt.Errorf(\"decode token response: %w\", err)\n\t}\n\tif result.ErrCode != 0 {\n\t\treturn \"\", fmt.Errorf(\"get token error: code=%d msg=%s\", result.ErrCode, result.ErrMsg)\n\t}\n\n\ta.tokenCache = result.AccessToken\n\t// Cache with 5-minute safety margin\n\tttl := time.Duration(result.ExpiresIn) * time.Second\n\tif ttl > 5*time.Minute {\n\t\tttl -= 5 * time.Minute\n\t}\n\ta.tokenExpAt = time.Now().Add(ttl)\n\n\treturn a.tokenCache, nil\n}\n\n// verifySignature verifies the WeCom callback signature using constant-time comparison.\nfunc (a *WebhookAdapter) verifySignature(signature, timestamp, nonce, encrypt string) bool {\n\tparts := []string{a.token, timestamp, nonce, encrypt}\n\tsort.Strings(parts)\n\tcombined := strings.Join(parts, \"\")","sourceCodeStart":402,"sourceCodeEnd":438,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/im/wecom/webhook_adapter.go#L402-L438","documentation":"getAccessToken fetches a WeCom access token using corpid + corpsecret and caches it. A non-zero errcode in the token response is wrapped as \"get token error: code=%d msg=%s\". This typically means the corp credentials used to obtain the token are wrong or the IP is not whitelisted.","triggerScenarios":"Called by SendReply and DownloadFile whenever a fresh token is needed and WeCom's gettoken endpoint returns errcode != 0 — e.g. 40001 invalid corpsecret, 40013 invalid corpid, 60020 IP not in the app's trusted IP list.","commonSituations":"Corp secret rotated in WeCom admin without updating channel credentials; typo in corp_id; server IP missing from the app's trusted-IP allowlist; clock skew invalidating tokens instantly.","solutions":["Check the errcode against WeCom docs: 40001 → fix corpsecret; 40013 → fix corpid; 60020 → add server IP to trusted IPs.","Verify the corpid and corpsecret in the channel credentials match the WeCom admin console for the correct app.","If credentials were rotated, update them and clear the adapter's tokenCache.","Ensure the outbound egress IP is stable and added to the app's trusted IP configuration."],"exampleFix":"// before\nch.Credentials = `{\"corp_id\":\"old-id\",\"corp_secret\":\"revoked-secret\"}`\n// after\nch.Credentials = `{\"corp_id\":\"ww-current-id\",\"corp_secret\":\"<current-secret>\"}`","handlingStrategy":"retry","validationCode":"if corpID == \"\" || corpSecret == \"\" {\n    return fmt.Errorf(\"wecom token fetch skipped: corp_id/corp_secret not configured\")\n}","typeGuard":null,"tryCatchPattern":"token, err := a.getAccessToken(ctx)\nif err != nil {\n    var code int\n    if n, _ := fmt.Sscanf(err.Error(), \"get token error: code=%d\", &code); n == 1 {\n        switch code {\n        case 40001:\n            return nil, fmt.Errorf(\"wecom corpsecret invalid — update channel credentials\")\n        case 40013:\n            return nil, fmt.Errorf(\"wecom corpid invalid — update channel credentials\")\n        case 60020:\n            return nil, fmt.Errorf(\"server IP not in wecom trusted IP list\")\n        }\n    }\n    return nil, err // transient network error — caller may retry\n}","preventionTips":["Rotate corpid/secret in both WeCom admin and channel credentials together.","Pin egress IPs and register them in the app's trusted IP list.","Cache tokens with a safety margin (as the adapter does) to reduce fetch frequency.","Monitor token-fetch failures separately from send failures to isolate auth issues."],"tags":["wecom","auth","access-token","api-error"],"backgroundTag":"access-token-fetch-failed","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}