{"record":{"id":"77da2f6c2284717c","repo":"chenhg5/cc-connect","slug":"wecom-ws-connection-closed","errorCode":null,"errorMessage":"wecom-ws: connection closed","messagePattern":"wecom-ws: connection closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/wecom/websocket.go","lineNumber":214,"sourceCode":"\tp.mu.Lock()\n\tp.conn = conn\n\tp.mu.Unlock()\n\n\tdefer func() {\n\t\tp.mu.Lock()\n\t\tp.conn = nil\n\t\tp.mu.Unlock()\n\t\tconn.Close()\n\n\t\t// Drain pending ACK channels so waiting goroutines are unblocked\n\t\t// and stale entries do not accumulate across reconnections.\n\t\t// Collect keys first, then delete — Range+Delete in callback is\n\t\t// not guaranteed safe by the sync.Map contract.\n\t\tvar staleKeys []any\n\t\tp.pendingAcks.Range(func(key, value any) bool {\n\t\t\tif ch, ok := value.(chan wsAckResult); ok {\n\t\t\t\tselect {\n\t\t\t\tcase ch <- wsAckResult{err: fmt.Errorf(\"wecom-ws: connection closed\")}:\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t\tstaleKeys = append(staleKeys, key)\n\t\t\treturn true\n\t\t})\n\t\tfor _, k := range staleKeys {\n\t\t\tp.pendingAcks.Delete(k)\n\t\t}\n\t}()\n\n\t// Send subscribe (auth) frame\n\t// Format: { cmd: \"aibot_subscribe\", headers: { req_id }, body: { bot_id, secret } }\n\tsubReqID := p.generateReqID(\"aibot_subscribe\")\n\tsubFrame := map[string]any{\n\t\t\"cmd\":     \"aibot_subscribe\",\n\t\t\"headers\": map[string]string{\"req_id\": subReqID},\n\t\t\"body\": map[string]string{","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/wecom/websocket.go#L196-L232","documentation":"When the WebSocket connection drops, runConnection's cleanup drains all pendingAck channels, delivering wsAckResult{err: 'wecom-ws: connection closed'} so callers waiting for reply/send acks fail fast instead of blocking forever. Senders blocked on an ack channel receive this error. Non-blocking send (select/default) means a channel nobody reads is simply discarded.","triggerScenarios":"A Send/Reply posted a frame with a req_id and is waiting on its ack channel when the connection dies (dial failure, read error, pong timeout) before the ack arrives.","commonSituations":"Network interruption while sending messages through the wecom-ws bot; server-side disconnect during a burst of sends; caller's ctx-less wait on a message that will never be acked after a drop.","solutions":["Treat it as transient: reconnect happens automatically via connectLoop; retry the send after reconnection.","Buffer or queue outbound messages and flush them once the 'subscribed successfully' log appears.","Check reconnect stability (frequent drops) — inspect preceding 'dial:'/'read:' errors for the root cause.","Ensure callers of the ack channels handle the error return rather than waiting indefinitely."],"exampleFix":"// before\nresult := <-ackCh // may block or get closed-chan zero value\n// after\nresult, ok := <-ackCh\nif !ok || (result.err != nil) {\n\treturn fmt.Errorf(\"wecom-ws: send not acked: %w\", result.err)\n}","handlingStrategy":"try-catch","validationCode":"if !platform.IsConnected() { queueForLater(msg); return } // check connection state before sending","typeGuard":"null","tryCatchPattern":"res, err := p.SendAndWaitAck(ctx, msg)\nif errors.Is(err, errConnClosed) {\n\twaitUntilSubscribed()\n\tres, err = p.SendAndWaitAck(ctx, msg) // retry once after reconnect\n}","preventionTips":["Queue outbound messages while disconnected and flush on 'subscribed successfully'","Handle ack-wait errors instead of blocking forever","Monitor reconnect frequency to detect unstable networks"],"tags":["wecom","websocket","disconnect","concurrency"],"backgroundTag":"websocket-connection-closed","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}