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

  1. 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.
  2. 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.
  3. For 81013/43004, verify the target userid exists and is within the app's visible range; print the toUser value being sent.
  4. For 45009, add backoff/rate limiting before retrying sends.
  5. 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

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


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