Skip to content

Commit 2e2d7ff

Browse files
authored
Merge pull request #4532 from ATKasem/fix/listener-fd-leak-error-paths
🐛 fix: close the listener on Listen error paths to avoid FD leaks
2 parents 33f91b3 + eb8eabe commit 2e2d7ff

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

listen.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,16 @@ func (app *App) Listen(addr string, config ...ListenConfig) error {
267267
return fmt.Errorf("failed to listen: %w", err)
268268
}
269269

270+
// Close the listener on any path that doesn't reach Serve (which otherwise
271+
// takes ownership of it) — an early error return or a panicking hook — so
272+
// the bound socket isn't leaked.
273+
served := false
274+
defer func() {
275+
if !served {
276+
_ = ln.Close() //nolint:errcheck // best-effort cleanup on the error path
277+
}
278+
}()
279+
270280
// prepare the server for the start
271281
app.startupProcess()
272282

@@ -285,6 +295,7 @@ func (app *App) Listen(addr string, config ...ListenConfig) error {
285295
}
286296
}
287297

298+
served = true
288299
return app.server.Serve(ln)
289300
}
290301

@@ -377,6 +388,7 @@ func (*App) createListener(addr string, tlsConfig *tls.Config, cfg *ListenConfig
377388

378389
if cfg.ListenerNetwork == NetworkUnix {
379390
if err = os.Chmod(addr, cfg.UnixSocketFileMode); err != nil {
391+
_ = listener.Close() //nolint:errcheck // best-effort cleanup on the error path
380392
return nil, fmt.Errorf("cannot chmod %#o for %q: %w", cfg.UnixSocketFileMode, addr, err)
381393
}
382394
}

listen_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,30 @@ func Test_Listen(t *testing.T) {
3838
require.NoError(t, app.Listen(":0", ListenConfig{DisableStartupMessage: true}))
3939
}
4040

41+
// go test -run Test_Listen_ClosesListenerOnBeforeServeError
42+
func Test_Listen_ClosesListenerOnBeforeServeError(t *testing.T) {
43+
// Grab a free port, then release it so we can bind it via Listen.
44+
probe, err := net.Listen("tcp", "127.0.0.1:0")
45+
require.NoError(t, err)
46+
addr := probe.Addr().String()
47+
require.NoError(t, probe.Close())
48+
49+
app := New()
50+
err = app.Listen(addr, ListenConfig{
51+
DisableStartupMessage: true,
52+
BeforeServeFunc: func(_ *App) error {
53+
return errors.New("stop before serving")
54+
},
55+
})
56+
require.Error(t, err)
57+
58+
// The listener must have been closed on the error path, so the port is
59+
// immediately bindable again (a leaked listener would keep it bound).
60+
ln, err := net.Listen("tcp", addr)
61+
require.NoError(t, err, "listener leaked: port still bound after BeforeServeFunc error")
62+
require.NoError(t, ln.Close())
63+
}
64+
4165
// go test -run Test_Listen_Graceful_Shutdown
4266
func Test_Listen_Graceful_Shutdown(t *testing.T) {
4367
t.Run("Basic Graceful Shutdown", func(t *testing.T) {

0 commit comments

Comments
 (0)