{"record":{"id":"025876bf7f269e65","repo":"chenhg5/cc-connect","slug":"wecom-ws-ack-timeout","errorCode":null,"errorMessage":"wecom-ws: ack timeout","messagePattern":"wecom-ws: ack timeout","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"platform/wecom/websocket.go","lineNumber":47,"sourceCode":"\tsecret      string\n\tallowFrom   string\n\tconn        *websocket.Conn\n\thandler     core.MessageHandler\n\tctx         context.Context\n\tcancel      context.CancelFunc\n\tmu          sync.Mutex // protects conn writes\n\tdedup       core.MessageDedup\n\treqSeq      atomic.Int64 // monotonic counter for generating unique req_id\n\tmissedPong  atomic.Int32 // consecutive heartbeat acks not received\n\tpendingAcks sync.Map     // req_id -> chan wsAckResult, for sequential send with ack waiting\n}\n\nconst (\n\twsAckTimeout      = 5 * time.Second\n\twsMediaAckTimeout = 30 * time.Second\n)\n\nvar errWSAckTimeout = errors.New(\"wecom-ws: ack timeout\")\n\n// wsReplyContext holds the context needed to reply to a specific message.\ntype wsReplyContext struct {\n\treqID    string // req_id from headers of aibot_msg_callback\n\tchatID   string // chatid for aibot_send_msg\n\tchatType string // chattype: \"single\" or \"group\"\n\tuserID   string // from.userid\n}\n\n// --- WebSocket protocol frame types (matching official SDK) ---\n\n// wsFrame is the unified frame structure used for all WebSocket communication.\n// Format: { cmd, headers: { req_id }, body: {...} }\n// Response frames may omit cmd and include errcode/errmsg instead.\ntype wsFrame struct {\n\tCmd     string          `json:\"cmd,omitempty\"`\n\tHeaders wsFrameHeaders  `json:\"headers\"`\n\tBody    json.RawMessage `json:\"body,omitempty\"`","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/wecom/websocket.go#L29-L65","documentation":"errWSAckTimeout is the sentinel for the WeCom (WeChat Work) websocket client: after sending a frame with a req_id, no ack/result frame arrived within the timeout (5s normal, 30s media). It is treated as a soft failure — writeAndWaitAckWithTimeout logs at debug and returns nil, proceeding without confirmation, while strict variants convert it into a descriptive error for the caller.","triggerScenarios":"writeAndWaitResult times out waiting for the ack keyed by reqID: slow or overloaded WeCom aibot gateway; network drop between client and WeCom; sending frames when the websocket connection is half-dead; media uploads exceeding wsMediaAckTimeout (30s).","commonSituations":"WeCom service degradation or throttling; corporate firewalls dropping long-lived websocket connections; message bursts where acks are processed out of order or slowly; media (image/file) uploads on slow uplinks.","solutions":["Check the debug log line 'wecom-ws: ack timeout, proceeding' frequency — if messages are being lost, treat it as a real delivery problem and inspect the WeCom service status.","Verify network stability to the WeCom gateway (persistent websocket, no aggressive NAT/firewall idle timeouts); enable keepalive/ping.","Increase wsAckTimeout/wsMediaAckTimeout if your uplink is slow, especially for media sends.","If strict mode reports it, retry the send with a fresh req_id after confirming the connection is alive (or reconnect first)."],"exampleFix":"// before\nif err := p.writeAndWaitAckStrict(ctx, frame, reqID, wsAckTimeout); err != nil { return err }\n// after\nif err := p.writeAndWaitAckStrict(ctx, frame, reqID, wsAckTimeout); err != nil {\n    if errors.Is(err, errWSAckTimeout) {\n        p.reconnectIfNeeded()\n        return p.writeAndWaitAckStrict(ctx, frame, reqID, wsAckTimeout)\n    }\n    return err\n}","handlingStrategy":"try-catch","validationCode":"// before sending, confirm the websocket is still alive:\nif p.conn == nil || time.Since(p.lastPong) > 2*wsAckTimeout {\n    if err := p.reconnect(ctx); err != nil {\n        return fmt.Errorf(\"wecom-ws: connection not ready: %w\", err)\n    }\n}","typeGuard":null,"tryCatchPattern":"err := p.writeAndWaitAckStrict(ctx, frame, reqID, wsAckTimeout)\nif errors.Is(err, errWSAckTimeout) {\n    slog.Warn(\"wecom-ws: ack not received\", \"req_id\", reqID)\n    p.reconnectIfNeeded()\n    // retry once with a fresh req_id\n    return p.writeAndWaitAckStrict(ctx, frame, newReqID(), wsAckTimeout)\n}","preventionTips":["Keep the websocket alive with periodic pings and detect dead connections early","Size wsAckTimeout/wsMediaAckTimeout to your network; media needs more than 5s","Monitor debug logs for 'ack timeout, proceeding' frequency as a delivery-health signal","Reconnect after repeated timeouts instead of continuing to send on a half-dead socket"],"tags":["wecom","websocket","timeout","ack","network"],"backgroundTag":"request-timeout","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}