{"record":{"id":"59814ed70bf78bba","repo":"fatedier/frp","slug":"listener-closed","errorCode":null,"errorMessage":"listener closed","messagePattern":"listener closed","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/util/net/listener.go","lineNumber":42,"sourceCode":"\n// InternalListener is a listener that can be used to accept connections from\n// other goroutines.\ntype InternalListener struct {\n\tacceptCh chan net.Conn\n\tclosed   bool\n\tmu       sync.Mutex\n}\n\nfunc NewInternalListener() *InternalListener {\n\treturn &InternalListener{\n\t\tacceptCh: make(chan net.Conn, 128),\n\t}\n}\n\nfunc (l *InternalListener) Accept() (net.Conn, error) {\n\tconn, ok := <-l.acceptCh\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"listener closed\")\n\t}\n\treturn conn, nil\n}\n\nfunc (l *InternalListener) PutConn(conn net.Conn) error {\n\terr := errors.PanicToError(func() {\n\t\tselect {\n\t\tcase l.acceptCh <- conn:\n\t\tdefault:\n\t\t\tconn.Close()\n\t\t}\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"put conn error: listener is closed\")\n\t}\n\treturn nil\n}\n","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/util/net/listener.go#L24-L60","documentation":"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.","triggerScenarios":"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.","commonSituations":"Shutdown ordering bugs where the accept loop is joined after (or never notified of) Close; tests that close listeners then call Accept once more.","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."],"exampleFix":"// before\nfunc (s *Server) serveLoop(l net.Listener) {\n    for {\n        c, err := l.Accept()\n        if err != nil {\n            panic(err) // crashes at shutdown\n        }\n        go s.handle(c)\n    }\n}\n\n// after\nfor {\n    c, err := l.Accept()\n    if err != nil {\n        return // listener closed — clean exit\n    }\n    go s.handle(c)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func isListenerClosedErr(err error) bool {\n    return errors.Is(err, net.ErrClosed) || err.Error() == \"listener closed\"\n}","tryCatchPattern":"conn, err := internalListener.Accept()\nif err != nil {\n    if isListenerClosedErr(err) {\n        return // terminal: channel was closed by Close()\n    }\n    log.Errorf(\"accept error: %v\", err)\n    continue\n}","preventionTips":["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."],"tags":["listener","shutdown","concurrency","internal"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}