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

  1. Check outbound network connectivity from the host: curl -v https://qyapi.weixin.qq.com/cgi-bin/message/send from the same machine.
  2. Verify the configured WeCom API base URL in config.toml — protocol must be https and the host resolvable.
  3. Inspect the wrapped error (errors.Unwrap / %v) for the concrete cause: 'connection refused' vs 'no such host' vs 'timeout' point to different fixes.
  4. Configure HTTP(S)_PROXY if the environment requires a proxy, and ensure the proxy allows POSTs to the WeCom domain.
  5. 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

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


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/053d1a334f316a05. Report an issue: GitHub.