fatedier/frp · warning
channel for kcp listener closed
Error message
channel for kcp listener closed
What it means
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.
Source
Thrown at pkg/util/net/kcp.go:70
continue
}
conn.SetStreamMode(true)
conn.SetWriteDelay(true)
conn.SetNoDelay(1, 20, 2, 1)
conn.SetMtu(1350)
conn.SetWindowSize(1024, 1024)
conn.SetACKNoDelay(false)
l.acceptCh <- conn
}
}()
return l, err
}
func (l *KCPListener) Accept() (net.Conn, error) {
conn, ok := <-l.acceptCh
if !ok {
return conn, fmt.Errorf("channel for kcp listener closed")
}
return conn, nil
}
func (l *KCPListener) Close() error {
if !l.closeFlag {
l.closeFlag = true
l.listener.Close()
}
return nil
}
func (l *KCPListener) Addr() net.Addr {
return l.listener.Addr()
}
func NewKCPConnFromUDP(conn *net.UDPConn, connected bool, raddr string) (net.Conn, error) {
udpAddr, err := net.ResolveUDPAddr("udp", raddr)View on GitHub (pinned to 6c8a8d0a97)
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.
Example fix
// before
for {
conn, err := l.Accept()
if err != nil {
log.Errorf("accept failed: %v", err) // noisy on shutdown
continue
}
go handle(conn)
}
// after
for {
conn, err := l.Accept()
if err != nil {
if strings.Contains(err.Error(), "listener closed") || strings.Contains(err.Error(), "channel for kcp listener closed") {
return // shutdown
}
log.Errorf("accept failed: %v", err)
continue
}
go handle(conn)
} Defensive patterns
Strategy: try-catch
Type guard
func isListenerClosedErr(err error) bool {
if errors.Is(err, net.ErrClosed) {
return true
}
msg := err.Error()
return strings.Contains(msg, "channel for kcp listener closed") ||
strings.Contains(msg, "listener closed")
} Try / catch
conn, err := kcpListener.Accept()
if err != nil {
if isListenerClosedErr(err) {
return // shutdown — stop the accept loop
}
log.Warnf("kcp accept error: %v", err)
continue
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: Graceful shutdown paths not checking for this error and logging it as unexpected; accept loops that retry on any error instead of terminating.
Related errors
- listener closed
- put conn error: listener is closed
- udp write closed listener
- control manager is closed
- dial udp error: %v
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/1946ed6d0bb1e0d2.
Report an issue: GitHub.