Skip to content

Commit f51819b

Browse files
fclairambclaude[bot]claude
authored
fix: avoid logging error when server stop is requested (#571)
* fix: avoid logging error when server stop is requested Fixes issue #498 where an error was logged when Stop() was called on the FTP server. The handleAcceptError method now checks for 'use of closed network connection' errors before logging, preventing unnecessary error logs during normal shutdown. - Move closed connection check before error logging in handleAcceptError - Add test to verify no error is logged during normal server stop Co-authored-by: Florent Clairambault <[email protected]> * fix: resolve linter issues across codebase - Fix errcheck issues by explicitly ignoring return values from disconnect() calls - Replace net.DialTimeout with context-aware dialer.DialContext - Replace net.Listen with net.ListenConfig.Listen using context - Fix unused parameter warnings by renaming to underscore - Remove unused nolint directive - Fix variable name length issue (wg -> waitGroup) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * test: use testing.T context instead of context.Background() Replace context.Background() with t.Context() in test files for proper test lifecycle management and better cancellation behavior. Co-authored-by: Florent Clairambault <[email protected]> * fix: correct misspelling of errUnknowHash to errUnknownHash Co-authored-by: Florent Clairambault <[email protected]> * refactor: modernize Go code with current best practices - Replace interface{} with any type alias (Go 1.18+) - Use range over integer for count-based loops (Go 1.22+) - Replace custom loop with slices.Contains - Remove unnecessary variable copying in test loops - Import slices package for slice utilities 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: remove unused context imports --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Florent Clairambault <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent afa2cf3 commit f51819b

11 files changed

+124
-30
lines changed

client_handler_test.go

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ftpserver
33
import (
44
"fmt"
55
"net"
6+
"slices"
67
"sync"
78
"testing"
89
"time"
@@ -20,7 +21,7 @@ func TestConcurrency(t *testing.T) {
2021
waitGroup := sync.WaitGroup{}
2122
waitGroup.Add(nbClients)
2223

23-
for i := 0; i < nbClients; i++ {
24+
for range nbClients {
2425
go func() {
2526
conf := goftp.Config{
2627
User: authUser,
@@ -47,7 +48,8 @@ func TestConcurrency(t *testing.T) {
4748

4849
func TestDOS(t *testing.T) {
4950
server := NewTestServer(t, true)
50-
conn, err := net.DialTimeout("tcp", server.Addr(), 5*time.Second)
51+
dialer := &net.Dialer{Timeout: 5 * time.Second}
52+
conn, err := dialer.DialContext(t.Context(), "tcp", server.Addr())
5153
require.NoError(t, err)
5254

5355
defer func() {
@@ -149,7 +151,8 @@ func TestConnectionNotAllowed(t *testing.T) {
149151
}
150152
s := NewTestServerWithTestDriver(t, driver)
151153

152-
conn, err := net.DialTimeout("tcp", s.Addr(), 5*time.Second)
154+
dialer := &net.Dialer{Timeout: 5 * time.Second}
155+
conn, err := dialer.DialContext(t.Context(), "tcp", s.Addr())
153156
require.NoError(t, err)
154157

155158
defer func() {
@@ -315,13 +318,7 @@ second line
315318
}
316319

317320
func isStringInSlice(s string, list []string) bool {
318-
for _, c := range list {
319-
if s == c {
320-
return true
321-
}
322-
}
323-
324-
return false
321+
return slices.Contains(list, s)
325322
}
326323

327324
func TestUnknownCommand(t *testing.T) {
@@ -486,7 +483,7 @@ func TestExtraData(t *testing.T) {
486483
require.Len(t, info, 1)
487484

488485
for k, v := range info {
489-
ccInfo, ok := v.(map[string]interface{})
486+
ccInfo, ok := v.(map[string]any)
490487
require.True(t, ok)
491488
extra, ok := ccInfo["extra"].(uint32)
492489
require.True(t, ok)

handle_auth.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ func (c *clientHandler) handleUSER(user string) error {
1111
err := verifier.PreAuthUser(c, user)
1212
if err != nil {
1313
c.writeMessage(StatusNotLoggedIn, fmt.Sprintf("User rejected: %v", err))
14-
c.disconnect()
14+
_ = c.disconnect()
1515

1616
return nil
1717
}
1818
}
1919

2020
if c.isTLSRequired() && !c.HasTLSForControl() {
2121
c.writeMessage(StatusServiceNotAvailable, "TLS is required")
22-
c.disconnect()
22+
_ = c.disconnect()
2323

2424
return nil
2525
}
@@ -52,7 +52,7 @@ func (c *clientHandler) handleUserTLS(user string) bool {
5252
driver, err := verifier.VerifyConnection(c, user, tlsConn)
5353
if err != nil {
5454
c.writeMessage(StatusNotLoggedIn, fmt.Sprintf("TLS verification failed: %v", err))
55-
c.disconnect()
55+
_ = c.disconnect()
5656

5757
return true
5858
}
@@ -82,14 +82,14 @@ func (c *clientHandler) handlePASS(param string) error {
8282
switch {
8383
case err == nil && c.driver == nil:
8484
c.writeMessage(StatusNotLoggedIn, "Unexpected exception (driver is nil)")
85-
c.disconnect()
85+
_ = c.disconnect()
8686
case err != nil:
8787
if msg == "" {
8888
msg = fmt.Sprintf("Authentication error: %v", err)
8989
}
9090

9191
c.writeMessage(StatusNotLoggedIn, msg)
92-
c.disconnect()
92+
_ = c.disconnect()
9393
default: // err == nil && c.driver != nil
9494
if msg == "" {
9595
msg = "Password ok, continue"

handle_auth_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ func panicOnError(err error) {
1919
func TestLoginSuccess(t *testing.T) {
2020
server := NewTestServer(t, false)
2121
// send a NOOP before the login, this doesn't seems possible using secsy/goftp so use the old way ...
22-
conn, err := net.DialTimeout("tcp", server.Addr(), 5*time.Second)
22+
dialer := &net.Dialer{Timeout: 5 * time.Second}
23+
conn, err := dialer.DialContext(t.Context(), "tcp", server.Addr())
2324
require.NoError(t, err)
2425

2526
defer func() {

handle_dirs_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,8 @@ func TestPerClientTLSTransfer(t *testing.T) {
460460

461461
func TestDirListingBeforeLogin(t *testing.T) {
462462
s := NewTestServer(t, false)
463-
conn, err := net.DialTimeout("tcp", s.Addr(), 5*time.Second)
463+
dialer := &net.Dialer{Timeout: 5 * time.Second}
464+
conn, err := dialer.DialContext(t.Context(), "tcp", s.Addr())
464465
require.NoError(t, err)
465466

466467
defer func() {

handle_files.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,7 @@ func (c *clientHandler) computeHashForFile(filePath string, algo HASHAlgo, start
689689
case HASHAlgoSHA512:
690690
chosenHashAlgo = sha512.New()
691691
default:
692-
return "", errUnknowHash
692+
return "", errUnknownHash
693693
}
694694

695695
file, err = c.getFileHandle(filePath, os.O_RDONLY, start)

handle_misc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"time"
1111
)
1212

13-
var errUnknowHash = errors.New("unknown hash algorithm")
13+
var errUnknownHash = errors.New("unknown hash algorithm")
1414

1515
func (c *clientHandler) handleAUTH(_ string) error {
1616
if tlsConfig, err := c.server.driver.GetTLSConfig(); err == nil {
@@ -316,7 +316,7 @@ func (c *clientHandler) handleQUIT(_ string) error {
316316
}
317317

318318
c.writeMessage(StatusClosingControlConn, msg)
319-
c.disconnect()
319+
_ = c.disconnect()
320320
c.reader = nil
321321

322322
return nil

server.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package ftpserver
33

44
import (
5+
"context"
56
"crypto/tls"
67
"errors"
78
"fmt"
@@ -208,7 +209,8 @@ func (server *FtpServer) Listen() error {
208209
}
209210

210211
func (server *FtpServer) createListener() (net.Listener, error) {
211-
listener, err := net.Listen("tcp", server.settings.ListenAddr)
212+
lc := &net.ListenConfig{}
213+
listener, err := lc.Listen(context.Background(), "tcp", server.settings.ListenAddr)
212214
if err != nil {
213215
server.Logger.Error("cannot listen on main port", "err", err, "listenAddr", server.settings.ListenAddr)
214216

@@ -266,8 +268,6 @@ func (server *FtpServer) Serve() error {
266268
// It returns a boolean indicating if the error should stop the server and the error itself or none if it's a standard
267269
// scenario (e.g. a closed listener)
268270
func (server *FtpServer) handleAcceptError(err error, tempDelay *time.Duration) (bool, error) {
269-
server.Logger.Error("Serve error", "err", err)
270-
271271
if errOp := (&net.OpError{}); errors.As(err, &errOp) {
272272
// This means we just closed the connection and it's OK
273273
if errOp.Err.Error() == "use of closed network connection" {
@@ -277,6 +277,8 @@ func (server *FtpServer) handleAcceptError(err error, tempDelay *time.Duration)
277277
}
278278
}
279279

280+
server.Logger.Error("Serve error", "err", err)
281+
280282
// see https://github.com/golang/go/blob/4aa1efed4853ea067d665a952eee77c52faac774/src/net/http/server.go#L3046
281283
// & https://github.com/fclairamb/ftpserverlib/pull/352#pullrequestreview-1077459896
282284
// The temporaryError method should replace net.Error.Temporary() when the go team

server_stop_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package ftpserver
2+
3+
import (
4+
"sync"
5+
"testing"
6+
"time"
7+
8+
log "github.com/fclairamb/go-log"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestServerStopDoesNotLogError tests that stopping a server doesn't log an error
13+
// when the listener is closed as expected
14+
func TestServerStopDoesNotLogError(t *testing.T) {
15+
req := require.New(t)
16+
17+
// Create a server with a test driver
18+
server := NewFtpServer(&TestServerDriver{
19+
Settings: &Settings{
20+
ListenAddr: "127.0.0.1:0", // Use dynamic port
21+
},
22+
})
23+
24+
// Use a custom logger that tracks error logs
25+
mockLogger := &MockLogger{}
26+
server.Logger = mockLogger
27+
28+
// Start listening
29+
err := server.Listen()
30+
req.NoError(err)
31+
32+
// Start serving in a goroutine
33+
var serveErr error
34+
var waitGroup sync.WaitGroup
35+
waitGroup.Add(1)
36+
37+
go func() {
38+
defer waitGroup.Done()
39+
serveErr = server.Serve()
40+
}()
41+
42+
// Give the server a moment to start accepting connections
43+
time.Sleep(100 * time.Millisecond)
44+
45+
// Stop the server
46+
err = server.Stop()
47+
req.NoError(err)
48+
49+
// Wait for the Serve goroutine to finish
50+
waitGroup.Wait()
51+
52+
// Serve should return nil (no error) when stopped normally
53+
req.NoError(serveErr)
54+
55+
// Check that no error was logged for the "use of closed network connection"
56+
// The mock logger should not have received any error logs
57+
req.Empty(mockLogger.ErrorLogs, "Expected no error logs when stopping server, but got: %v", mockLogger.ErrorLogs)
58+
}
59+
60+
// MockLogger captures log calls to verify behavior
61+
type MockLogger struct {
62+
ErrorLogs []string
63+
WarnLogs []string
64+
InfoLogs []string
65+
DebugLogs []string
66+
}
67+
68+
func (m *MockLogger) Debug(message string, _ ...any) {
69+
m.DebugLogs = append(m.DebugLogs, message)
70+
}
71+
72+
func (m *MockLogger) Info(message string, _ ...any) {
73+
m.InfoLogs = append(m.InfoLogs, message)
74+
}
75+
76+
func (m *MockLogger) Warn(message string, _ ...any) {
77+
m.WarnLogs = append(m.WarnLogs, message)
78+
}
79+
80+
func (m *MockLogger) Error(message string, _ ...any) {
81+
m.ErrorLogs = append(m.ErrorLogs, message)
82+
}
83+
84+
func (m *MockLogger) Panic(message string, _ ...any) {
85+
panic(message)
86+
}
87+
88+
func (m *MockLogger) With(_ ...any) log.Logger {
89+
return m
90+
}

server_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ func newFakeListener(err error) net.Listener {
8484
func TestCannotListen(t *testing.T) {
8585
req := require.New(t)
8686

87-
portBlockerListener, err := net.Listen("tcp", "127.0.0.1:0")
87+
lc := &net.ListenConfig{}
88+
portBlockerListener, err := lc.Listen(t.Context(), "tcp", "127.0.0.1:0")
8889
req.NoError(err)
8990

9091
defer func() { req.NoError(portBlockerListener.Close()) }()
@@ -107,7 +108,8 @@ func TestCannotListen(t *testing.T) {
107108
func TestListenWithBadTLSSettings(t *testing.T) {
108109
req := require.New(t)
109110

110-
portBlockerListener, err := net.Listen("tcp", "127.0.0.1:0")
111+
lc := &net.ListenConfig{}
112+
portBlockerListener, err := lc.Listen(t.Context(), "tcp", "127.0.0.1:0")
111113
req.NoError(err)
112114

113115
defer func() { req.NoError(portBlockerListener.Close()) }()
@@ -173,7 +175,6 @@ func TestQuoteDoubling(t *testing.T) {
173175
}
174176

175177
for _, tt := range tests {
176-
tt := tt
177178
t.Run(tt.name, func(t *testing.T) {
178179
require.Equal(t, tt.want, quoteDoubling(tt.args.s))
179180
})

transfer_active_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ func TestRemoteAddrFormat(t *testing.T) {
2626
}
2727

2828
func TestActiveTransferFromPort20(t *testing.T) {
29-
listener, err := net.Listen("tcp", ":20") //nolint:gosec
29+
lc := &net.ListenConfig{}
30+
listener, err := lc.Listen(t.Context(), "tcp", ":20")
3031
if err != nil {
3132
t.Skipf("Binding on port 20 is not supported here: %v", err)
3233
}

0 commit comments

Comments
 (0)