chenhg5/cc-connect · error
wecom: send message: %w
Error message
wecom: send message: %w
What it means
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'.
Source
Thrown at platform/wecom/wecom.go:640
}
func (p *Platform) sendText(accessToken, toUser, text string) error {
payload := map[string]any{
"touser": toUser,
"msgtype": "text",
"agentid": p.agentID,
"text": map[string]string{"content": text},
"safe": 0,
}
body, _ := json.Marshal(payload)
apiURL := p.wecomAPIURL("/cgi-bin/message/send", url.Values{
"access_token": []string{accessToken},
})
resp, err := p.apiClient.Post(apiURL, "application/json", strings.NewReader(string(body)))
if err != nil {
return fmt.Errorf("wecom: send message: %w", err)
}
defer resp.Body.Close()
var result struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("wecom: decode send response: %w", err)
}
if result.ErrCode != 0 {
return fmt.Errorf("wecom: send failed: %d %s", result.ErrCode, result.ErrMsg)
}
return nil
}
func (p *Platform) getAccessToken() (string, error) {
p.tokenCache.mu.Lock()View on GitHub (pinned to 4000b2338a)
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).
Example fix
// before
resp, err := p.apiClient.Post(apiURL, "application/json", strings.NewReader(string(body)))
if err != nil {
return fmt.Errorf("wecom: send message: %w", err)
}
// after — timeout-bounded client + retry on transient errors
p.apiClient = &http.Client{Timeout: 10 * time.Second}
var resp *http.Response
for attempt := 0; attempt < 3; attempt++ {
resp, err = p.apiClient.Post(apiURL, "application/json", strings.NewReader(string(body)))
if err == nil {
break
}
if !isTransientNetErr(err) {
return fmt.Errorf("wecom: send message: %w", err)
}
time.Sleep(time.Duration(1<<attempt) * time.Second)
}
if err != nil {
return fmt.Errorf("wecom: send message after retries: %w", err)
} Defensive patterns
Strategy: retry
Validate before calling
// Reachability pre-check before relying on sends (run at startup or in doctor):
req, _ := http.NewRequest(http.MethodGet, "https://qyapi.weixin.qq.com/cgi-bin/gettoken", nil)
client := &http.Client{Timeout: 5 * time.Second}
if _, err := client.Do(req); err != nil {
// egress to WeCom API is broken: DNS/proxy/firewall problem
} Type guard
func isTransportErr(err error) bool {
return err != nil && strings.Contains(err.Error(), "wecom: send message:")
} Try / catch
if err := p.Reply(msg, text); err != nil {
if isTransportErr(err) {
// network-level failure: retry with exponential backoff
for i := 0; i < 3; i++ {
time.Sleep(time.Duration(1<<i) * time.Second)
if retryErr := p.Reply(msg, text); retryErr == nil {
return nil
}
}
slog.Error("wecom send failed after retries", "err", err)
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- reasonix: POST %s: %w
- wecom-ws: download HTTP %s
- range chunk retries exhausted
- wecom-ws: ack timeout
- connect permission bridge: %w
AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06).
Data as JSON: /api/errors/053d1a334f316a05.
Report an issue: GitHub.