{"record":{"id":"1946ed6d0bb1e0d2","repo":"fatedier/frp","slug":"channel-for-kcp-listener-closed","errorCode":null,"errorMessage":"channel for kcp listener closed","messagePattern":"channel for kcp listener closed","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/util/net/kcp.go","lineNumber":70,"sourceCode":"\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconn.SetStreamMode(true)\n\t\t\tconn.SetWriteDelay(true)\n\t\t\tconn.SetNoDelay(1, 20, 2, 1)\n\t\t\tconn.SetMtu(1350)\n\t\t\tconn.SetWindowSize(1024, 1024)\n\t\t\tconn.SetACKNoDelay(false)\n\n\t\t\tl.acceptCh <- conn\n\t\t}\n\t}()\n\treturn l, err\n}\n\nfunc (l *KCPListener) Accept() (net.Conn, error) {\n\tconn, ok := <-l.acceptCh\n\tif !ok {\n\t\treturn conn, fmt.Errorf(\"channel for kcp listener closed\")\n\t}\n\treturn conn, nil\n}\n\nfunc (l *KCPListener) Close() error {\n\tif !l.closeFlag {\n\t\tl.closeFlag = true\n\t\tl.listener.Close()\n\t}\n\treturn nil\n}\n\nfunc (l *KCPListener) Addr() net.Addr {\n\treturn l.listener.Addr()\n}\n\nfunc NewKCPConnFromUDP(conn *net.UDPConn, connected bool, raddr string) (net.Conn, error) {\n\tudpAddr, err := net.ResolveUDPAddr(\"udp\", raddr)","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/util/net/kcp.go#L52-L88","documentation":"KCPListener.Accept receives from an acceptCh that the accept goroutine fills; when the channel is closed (by KCPListener.Close), the receive yields ok=false and this error is returned. It is the standard 'listener closed' sentinel for KCP mode, analogous to net.ErrClosed for TCP listeners: accept loops should stop on it.","triggerScenarios":"Calling Accept() after Close() on a KCP listener, or concurrently with Close() — the race resolves to either one final conn or this error. Happens during frps shutdown when transport.protocol = kcp.","commonSituations":"Graceful shutdown paths not checking for this error and logging it as unexpected; accept loops that retry on any error instead of terminating.","solutions":["Treat this error as a stop signal in accept loops: break/return, do not retry.","Compare by message or wrap KCPListener Close/Accept with your own closed flag/errgroup cancellation if you need errors.Is semantics.","Ensure Close() is called exactly once (guarded by closeFlag) before joining accept goroutines."],"exampleFix":"// before\nfor {\n    conn, err := l.Accept()\n    if err != nil {\n        log.Errorf(\"accept failed: %v\", err) // noisy on shutdown\n        continue\n    }\n    go handle(conn)\n}\n\n// after\nfor {\n    conn, err := l.Accept()\n    if err != nil {\n        if strings.Contains(err.Error(), \"listener closed\") || strings.Contains(err.Error(), \"channel for kcp listener closed\") {\n            return // shutdown\n        }\n        log.Errorf(\"accept failed: %v\", err)\n        continue\n    }\n    go handle(conn)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func isListenerClosedErr(err error) bool {\n    if errors.Is(err, net.ErrClosed) {\n        return true\n    }\n    msg := err.Error()\n    return strings.Contains(msg, \"channel for kcp listener closed\") ||\n        strings.Contains(msg, \"listener closed\")\n}","tryCatchPattern":"conn, err := kcpListener.Accept()\nif err != nil {\n    if isListenerClosedErr(err) {\n        return // shutdown — stop the accept loop\n    }\n    log.Warnf(\"kcp accept error: %v\", err)\n    continue\n}","preventionTips":["Treat 'listener closed' errors as terminal stop signals in accept loops.","Close listeners before joining accept goroutines during shutdown.","Centralize the closed-error check in one helper since KCP/Internal listeners use string sentinels, not net.ErrClosed."],"tags":["kcp","listener","shutdown","concurrency"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}