chenhg5/cc-connect · error

wecom-ws: ack error: errcode=%d errmsg=%s

Error message

wecom-ws: ack error: errcode=%d errmsg=%s

What it means

handleFrame dispatches ack frames for outstanding reply/send requests (matched by req_id). When the ack frame carries a non-zero errcode, this error is built with the server's errcode/errmsg and delivered to the waiting caller via dispatchAck. It means the specific message send/reply was rejected by WeCom at the application level — the connection and subscription are fine.

Source

Thrown at platform/wecom/websocket.go:304

	switch frame.Cmd {
	case "aibot_msg_callback":
		p.handleMsgCallback(frame)
	case "aibot_event_callback":
		slog.Debug("wecom-ws: event callback received (ignored)", "req_id", frame.Headers.ReqID)
	case "":
		// Response frame (no cmd): identify by req_id prefix
		reqID := frame.Headers.ReqID
		switch {
		case strings.HasPrefix(reqID, "ping"):
			p.missedPong.Store(0)
			slog.Debug("wecom-ws: heartbeat ack received")
		case strings.HasPrefix(reqID, "aibot_subscribe"):
			// Late subscribe ack (should have been consumed in runConnection)
			slog.Debug("wecom-ws: late subscribe ack")
		default:
			var ackErr error
			if frame.ErrCode != nil && *frame.ErrCode != 0 {
				ackErr = fmt.Errorf("wecom-ws: ack error: errcode=%d errmsg=%s", *frame.ErrCode, frame.ErrMsg)
				slog.Warn("wecom-ws: reply/send ack error", "req_id", reqID, "errcode", *frame.ErrCode, "errmsg", frame.ErrMsg)
			} else {
				slog.Debug("wecom-ws: reply/send ack ok", "req_id", reqID)
			}
			p.dispatchAck(reqID, wsAckResult{frame: frame, err: ackErr})
		}
	default:
		slog.Debug("wecom-ws: unhandled cmd", "cmd", frame.Cmd)
	}
}

func (p *WSPlatform) dispatchAck(reqID string, result wsAckResult) {
	ch, ok := p.pendingAcks.LoadAndDelete(reqID)
	if !ok {
		return
	}
	resultCh, ok := ch.(chan wsAckResult)
	if !ok {

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Log/inspect errcode and look it up in WeCom's error code docs to identify the rejection reason.
  2. Verify the target user/chat ID is valid and the bot is permitted to message it (user must have interacted with the bot first for 1:1 messages).
  3. Check message content for prohibited or oversized payload rejected by the server.
  4. Surface the error to the caller (dispatchAck already does) and add retry logic only for retryable errcodes.

Example fix

// before
p.dispatchAck(reqID, wsAckResult{frame: frame, err: ackErr}) // caller ignores ackErr
// after
res := <-ackCh
if res.err != nil {
	slog.Error("wecom send rejected", "req_id", reqID, "err", res.err)
	return res.err
}
Defensive patterns

Strategy: try-catch

Validate before calling

// before sending: ensure target is valid and has interacted with the bot
if !botKnowsUser(personID) { return errors.New("user must message the bot first") }

Type guard

null

Try / catch

res, err := p.SendAndWaitAck(ctx, msg)
if res.err != nil {
	switch {
	case isRetryableAckCode(res.err): retryWithBackoff()
	default: return fmt.Errorf("wecom rejected send: %w", res.err)
	}
}

Prevention

When it happens

Trigger: A reply/send frame got an ack with errcode != 0 — e.g. invalid target chat/user ID, message content rejected, permission error, or expired req_id/session on the server side.

Common situations: Bot not authorized to message the target user (user never messaged the bot first); malformed markdown/text content rejected; sending to a chat the bot was removed from; content violating platform policies.

Related errors


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