{"record":{"id":"053d1a334f316a05","repo":"chenhg5/cc-connect","slug":"wecom-send-message-w","errorCode":null,"errorMessage":"wecom: send message: %w","messagePattern":"wecom: send message: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/wecom/wecom.go","lineNumber":640,"sourceCode":"}\n\nfunc (p *Platform) sendText(accessToken, toUser, text string) error {\n\tpayload := map[string]any{\n\t\t\"touser\":  toUser,\n\t\t\"msgtype\": \"text\",\n\t\t\"agentid\": p.agentID,\n\t\t\"text\":    map[string]string{\"content\": text},\n\t\t\"safe\":    0,\n\t}\n\n\tbody, _ := json.Marshal(payload)\n\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()","sourceCodeStart":622,"sourceCodeEnd":658,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/wecom/wecom.go#L622-L658","documentation":"This error wraps a transport-level failure of the HTTP POST performed by sendText to the WeCom /cgi-bin/message/send endpoint. It is returned when apiClient.Post itself fails — DNS resolution errors, connection refused/reset, TLS handshake failure, or request timeout — before any response is received. Because Decode has not run yet, no errcode exists; the wrapped error (%w) contains the low-level cause such as 'dial tcp: connection refused'.","triggerScenarios":"sendText calls p.apiClient.Post(apiURL, \"application/json\", strings.NewReader(string(body))) where apiURL is built by wecomAPIURL with the access_token query param. This error fires when the Post call returns a non-nil err: unreachable host, refused connection, DNS failure, proxy misconfiguration, TLS certificate problems, or client-side timeout.","commonSituations":"1) server running in a network without outbound access to qyapi.weixin.qq.com; 2) DNS failures or IPv6 routing issues; 3) corporate firewall/proxy blocking the request; 4) wrong custom API base URL in config (typo'd domain, http instead of https); 5) WeCom API outage or local network flakiness.","solutions":["Check outbound network connectivity from the host: curl -v https://qyapi.weixin.qq.com/cgi-bin/message/send from the same machine.","Verify the configured WeCom API base URL in config.toml — protocol must be https and the host resolvable.","Inspect the wrapped error (errors.Unwrap / %v) for the concrete cause: 'connection refused' vs 'no such host' vs 'timeout' point to different fixes.","Configure HTTP(S)_PROXY if the environment requires a proxy, and ensure the proxy allows POSTs to the WeCom domain.","Add retry with exponential backoff for transient network errors (sendText has no built-in retry)."],"exampleFix":"// before\nresp, err := p.apiClient.Post(apiURL, \"application/json\", strings.NewReader(string(body)))\nif err != nil {\n    return fmt.Errorf(\"wecom: send message: %w\", err)\n}\n// after — timeout-bounded client + retry on transient errors\np.apiClient = &http.Client{Timeout: 10 * time.Second}\nvar resp *http.Response\nfor attempt := 0; attempt < 3; attempt++ {\n    resp, err = p.apiClient.Post(apiURL, \"application/json\", strings.NewReader(string(body)))\n    if err == nil {\n        break\n    }\n    if !isTransientNetErr(err) {\n        return fmt.Errorf(\"wecom: send message: %w\", err)\n    }\n    time.Sleep(time.Duration(1<<attempt) * time.Second)\n}\nif err != nil {\n    return fmt.Errorf(\"wecom: send message after retries: %w\", err)\n}","handlingStrategy":"retry","validationCode":"// Reachability pre-check before relying on sends (run at startup or in doctor):\nreq, _ := http.NewRequest(http.MethodGet, \"https://qyapi.weixin.qq.com/cgi-bin/gettoken\", nil)\nclient := &http.Client{Timeout: 5 * time.Second}\nif _, err := client.Do(req); err != nil {\n    // egress to WeCom API is broken: DNS/proxy/firewall problem\n}","typeGuard":"func isTransportErr(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"wecom: send message:\")\n}","tryCatchPattern":"if err := p.Reply(msg, text); err != nil {\n    if isTransportErr(err) {\n        // network-level failure: retry with exponential backoff\n        for i := 0; i < 3; i++ {\n            time.Sleep(time.Duration(1<<i) * time.Second)\n            if retryErr := p.Reply(msg, text); retryErr == nil {\n                return nil\n            }\n        }\n        slog.Error(\"wecom send failed after retries\", \"err\", err)\n    }\n}","preventionTips":["Configure the http.Client with an explicit Timeout; unbounded clients hang on blackholed connections.","Verify egress (DNS + firewall + proxy) to qyapi.weixin.qq.com from the deployment host during setup.","Use https:// (never http://) in any custom API base URL to avoid TLS/proxy surprises.","Build in retry-with-backoff for transient transport errors; these are the most common false alerts."],"tags":["network","http","wecom","connection"],"backgroundTag":"network-request-failed","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}