chenhg5/cc-connect · error
wecom: send markdown failed: %d %s
Error message
wecom: send markdown failed: %d %s
What it means
This error is returned by sendMarkdown when the WeCom /cgi-bin/message/send API accepted the HTTP request but rejected the business request: the JSON response carried a non-zero errcode. The error message embeds the numeric errcode and the server-provided errmsg, e.g. 'wecom: send markdown failed: 40008 invalid message type'. The errcode is the authoritative diagnosis — it distinguishes auth problems (40014 invalid access_token), bad targets (43004, 81013 invalid user), rate limits (45009), and payload problems (40008).
Source
Thrown at platform/wecom/wecom.go:619
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 markdown: %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 markdown failed: %d %s", result.ErrCode, result.ErrMsg)
}
return nil
}
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},
})
View on GitHub (pinned to 4000b2338a)
Solutions
- Read the errcode in the message and match it against WeCom's error-code table (https://developer.work.weixin.qq.com/document/path/90313) — each code has a documented cause.
- For 40014/42001, force a token refresh: clear the token cache or wait for getAccessToken's expiry logic, and verify corpid/corpsecret in config.toml.
- For 81013/43004, verify the target userid exists and is within the app's visible range; print the toUser value being sent.
- For 45009, add backoff/rate limiting before retrying sends.
- For 40008/40058, validate the markdown payload (length, characters) and reduce message size or simplify formatting.
Example fix
// before
err := p.Reply(...) // sendMarkdown fails: wecom: send markdown failed: 40014 invalid credential
// after — refresh token before send
tok, err := p.getAccessToken()
if err != nil {
return fmt.Errorf("wecom: reply: %w", err)
}
if wecomErrCode(err) == 40014 || wecomErrCode(err) == 42001 { // parse from wrapped error or extend result handling
p.invalidateTokenCache()
tok, err = p.getAccessToken()
if err != nil {
return fmt.Errorf("wecom: refresh token: %w", err)
}
return p.sendMarkdown(tok, toUser, md)
} Defensive patterns
Strategy: try-catch
Validate before calling
// Validate prerequisites before sending markdown:
// 1) token is fresh, 2) target user is set, 3) payload is non-empty and within size limits
if accessToken == "" || toUser == "" || strings.TrimSpace(markdown) == "" {
return fmt.Errorf("wecom: pre-send validation failed: empty token/user/content")
}
if len(markdown) > 4096 { // WeCom content limit guard
return fmt.Errorf("wecom: markdown too large: %d bytes", len(markdown))
} Type guard
func wecomAPIErrCode(err error) (int, bool) {
if err == nil {
return 0, false
}
var code int
if _, scanErr := fmt.Sscanf(err.Error(), "wecom: send markdown failed: %d", &code); scanErr == nil {
return code, true
}
return 0, false
} Try / catch
if err := p.Reply(msg, md); err != nil {
if code, ok := wecomAPIErrCode(err); ok {
switch code {
case 40014, 42001:
p.invalidateTokenCache(); // retry once with fresh token
case 81013:
slog.Warn("wecom: invalid target userid", "toUser", toUser)
case 45009:
time.Sleep(backoff); // rate limited: retry later
default:
slog.Error("wecom: send markdown rejected", "errcode", code, "errmsg", err)
}
}
} Prevention
- Monitor errcodes and alert on 40014/42001 (token) vs 81013 (user) — they have completely different remediations.
- Keep corpid/corpsecret/agentid in config.toml verified against the WeCom admin console; rotate secrets with a token-cache flush.
- Confirm every configured target userid is inside the app's visible scope before deployment.
- Throttle outbound sends to stay under WeCom API rate limits.
When it happens
Trigger: Reply -> sendMarkdown posts a markdown payload whose JSON response contains result.ErrCode != 0. Typical errcodes: 40014/42001 expired or invalid access_token; 40008 markdown content invalid; 81013 invalid userid list (toUser); 45009 API rate limit exceeded; 40058 invalid parameter format.
Common situations: 1) access_token cache expired mid-flight or corpsecret rotated; 2) messaging a user whose account was disabled/deleted or whose userid is misspelled in config; 3) markdown body exceeding WeCom's size limits or containing unsupported constructs; 4) hitting qyapi rate limits during bot storms; 5) app visibility — the target user is not in the app's visible scope.
Related errors
- wecom: send failed: %d %s
- webex: postMessage status %d
- wecom-ws: ack error: errcode=%d errmsg=%s
- wecom-ws: ack timeout
- usage endpoint returned status %d: %s
AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06).
Data as JSON: /api/errors/a7f4c7ac8704c867.
Report an issue: GitHub.