fatedier/frp · warning
listener closed
Error message
listener closed
What it means
InternalListener is a net.Listener whose connections are injected via PutConn rather than a socket. Accept receives from acceptCh; once Close() closes the channel, Accept returns this 'listener closed' error permanently. It exists so frp can present in-process connections through the standard Listener interface.
Source
Thrown at pkg/util/net/listener.go:42
// InternalListener is a listener that can be used to accept connections from
// other goroutines.
type InternalListener struct {
acceptCh chan net.Conn
closed bool
mu sync.Mutex
}
func NewInternalListener() *InternalListener {
return &InternalListener{
acceptCh: make(chan net.Conn, 128),
}
}
func (l *InternalListener) Accept() (net.Conn, error) {
conn, ok := <-l.acceptCh
if !ok {
return nil, fmt.Errorf("listener closed")
}
return conn, nil
}
func (l *InternalListener) PutConn(conn net.Conn) error {
err := errors.PanicToError(func() {
select {
case l.acceptCh <- conn:
default:
conn.Close()
}
})
if err != nil {
return fmt.Errorf("put conn error: listener is closed")
}
return nil
}
View on GitHub (pinned to 6c8a8d0a97)
Solutions
- Stop the accept loop when this error is observed — it is terminal, not retriable.
- Close the listener before waiting on accept-loop goroutines so the blocked Accept wakes with this error.
- If you need sentinel comparison, check err.Error() == "listener closed" or wrap InternalListener to return net.ErrClosed.
Example fix
// before
func (s *Server) serveLoop(l net.Listener) {
for {
c, err := l.Accept()
if err != nil {
panic(err) // crashes at shutdown
}
go s.handle(c)
}
}
// after
for {
c, err := l.Accept()
if err != nil {
return // listener closed — clean exit
}
go s.handle(c)
} Defensive patterns
Strategy: try-catch
Type guard
func isListenerClosedErr(err error) bool {
return errors.Is(err, net.ErrClosed) || err.Error() == "listener closed"
} Try / catch
conn, err := internalListener.Accept()
if err != nil {
if isListenerClosedErr(err) {
return // terminal: channel was closed by Close()
}
log.Errorf("accept error: %v", err)
continue
} Prevention
- Order shutdown: Close() the InternalListener first so blocked Accepts wake with this error, then join goroutines.
- Never retry Accept on this error — the channel is closed permanently.
- If you need errors.Is support, wrap InternalListener and return net.ErrClosed instead of the string.
When it happens
Trigger: Calling Accept after Close on an InternalListener — e.g. the https-to-http fallback or other virtual hosts being torn down at frps shutdown while the accept loop is still blocked in <-acceptCh.
Common situations: Shutdown ordering bugs where the accept loop is joined after (or never notified of) Close; tests that close listeners then call Accept once more.
Related errors
- channel for kcp listener closed
- put conn error: listener is closed
- udp write closed listener
- control manager is closed
- apply config failed: proxy %q not found in store after mutat
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/59814ed70bf78bba.
Report an issue: GitHub.