gofr-dev/gofr · error
writer already closed
Error message
writer already closed
What it means
errWriterAlreadyClosed is returned by the writer's Write method after Close (or Commit) has already been called. FTP writers are single-use: once closed, the underlying transfer connection is finished and cannot accept more data. This is a client-side usage error, not a server error.
Source
Thrown at pkg/gofr/datasource/file/ftp/storage_adapter.go:32
"gofr.dev/pkg/gofr/datasource/file"
)
var (
// Storage adapter errors.
errFTPConfigNil = errors.New("FTP config is nil")
errFTPClientNotInitialized = errors.New("FTP client is not initialized")
errEmptyObjectName = errors.New("object name is empty")
errInvalidOffset = errors.New("invalid offset: must be >= 0")
errEmptySourceOrDest = errors.New("source and destination names cannot be empty")
errSameSourceAndDest = errors.New("source and destination are the same")
errFailedToCreateReader = errors.New("failed to create reader")
errFailedToCreateWriter = errors.New("failed to create writer")
errObjectNotFound = errors.New("object not found")
errFailedToGetObjectAttrs = errors.New("failed to get object attrs")
errFailedToDeleteObject = errors.New("failed to delete object")
errFailedToListObjects = errors.New("failed to list objects")
errFailedToListDirectory = errors.New("failed to list directory")
errWriterAlreadyClosed = errors.New("writer already closed")
errFTPConfigInvalid = errors.New("invalid FTP configuration: host and port are required")
)
// Config represents the FTP configuration.
type Config struct {
Host string // FTP server hostname
User string // FTP username
Password string // FTP password
Port int // FTP port
RemoteDir string // Remote directory path. Base Path for all FTP Operations.
DialTimeout time.Duration // FTP connection timeout
}
// storageAdapter adapts FTP client to implement file.StorageProvider.
type storageAdapter struct {
cfg *Config
conn *ftp.ServerConn
}View on GitHub (pinned to 187eb24962)
Solutions
- Create a new writer via NewWriter for each upload instead of reusing a closed one
- Reorder code so all Write calls happen before Close
- Remove duplicate Close/defer-Close combinations that close the writer early
- Check writer state (closed flag) before writing in wrapper code
Example fix
// before w, _ := fs.NewWriter(ctx, name, nil) defer w.Close() w.Write(data1) w.Write(data2) // panics/error: writer already closed // after w, _ := fs.NewWriter(ctx, name, nil) w.Write(data1) w.Write(data2) w.Close()
Defensive patterns
Strategy: type-guard
Validate before calling
// track writer lifecycle explicitly
type trackedWriter struct { w *ftp.Writer; closed bool } Type guard
func isClosedWriterError(err error) bool { return errors.Is(err, ftp.ErrWriterAlreadyClosed) } Try / catch
if _, err := w.Write(data); errors.Is(err, ftp.ErrWriterAlreadyClosed) {
// create a fresh writer and redo the upload
w, werr = fs.NewWriter(ctx, name, nil)
} Prevention
- Treat writers as single-use: one writer per upload, never reuse
- Close exactly once, after all writes; avoid mixing defer Close with later writes
- Wrap the write-then-close sequence in a helper function to enforce ordering
- On retry after failure, create a new writer rather than reusing the closed one
When it happens
Trigger: Calling Write on an ftp writer after Close/Commit; calling Write twice across a code path where Close is invoked in a defer and then written to again; reusing a cached writer object for a second upload.
Common situations: defer w.Close() placed before a later write in the same function; error-retry loops that reuse the closed writer instead of creating a new one; long-lived writer structs stored in application state and reused.
Related errors
- FTP client is not initialized
- failed to create reader
- failed to create writer
- invalid FTP configuration: host and port are required
- invalid FTP provider
AI-assisted analysis of gofr-dev/gofr@187eb24962 (2026-09-01).
Data as JSON: /api/errors/4328945e1f44e227.
Report an issue: GitHub.