Billionmail/BillionMail · error
failed to close zip writer: %v
Error message
failed to close zip writer: %v
What it means
archive/zip's Writer.Close finalizes the archive: it writes the central directory and any buffered data. Close fails if any previously written entry returned an error on flush/close, or if the underlying writer (bytes.Buffer here) errors — which for an in-memory buffer is essentially impossible, making this a defensive guard that matters only if the sink changes.
Source
Thrown at core/internal/controller/contact/contact_v1_export_contacts.go:229
zipWriter := zip.NewWriter(&buf)
for _, file := range files {
// Create zip entry
zipEntry, err := zipWriter.Create(file.Name)
if err != nil {
return nil, fmt.Errorf("failed to create zip entry for %s: %v", file.Name, err)
}
// Write file content to zip
_, err = zipEntry.Write([]byte(file.Content))
if err != nil {
return nil, fmt.Errorf("failed to write content to zip for %s: %v", file.Name, err)
}
}
err := zipWriter.Close()
if err != nil {
return nil, fmt.Errorf("failed to close zip writer: %v", err)
}
return buf.Bytes(), nil
}
// exportContactsToCSV exports contacts to CSV format
func exportContactsToCSV(contacts []*entity.Contact) (string, error) {
var buf bytes.Buffer
writer := csv.NewWriter(&buf)
// Write CSV headers
headers := []string{
"email", // Email address
"attributes", // Attributes
"active", // Active status
"create_time", // Create time
}
if err := writer.Write(headers); err != nil {View on GitHub (pinned to fc36c76c05)
Solutions
- With the current in-memory buffer this should never fire; if it does after changing the sink, verify disk space and file permissions.
- Call zipWriter.Close exactly once, before returning buf.Bytes(); never rely on defer alone if the return value depends on Close.
- Wrap with %w to let callers distinguish close errors from entry errors.
- If streaming to HTTP, flush the response writer before Close and handle client-abort errors.
- Validate the produced ZIP (e.g. zip.NewReader) in tests to catch silent truncation regressions.
Example fix
// before
err := zipWriter.Close()
if err != nil {
return nil, fmt.Errorf("failed to close zip writer: %v", err)
}
// after
if err := zipWriter.Close(); err != nil {
return nil, fmt.Errorf("failed to close zip writer: %w", err)
}
if buf.Len() == 0 {
return nil, fmt.Errorf("zip writer closed with empty output")
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-flight: ensure entries exist and names are valid so Close-time state is clean
if len(files) == 0 { return errors.New("nothing to export") }
for _, f := range files {
if utf8.ValidString(f.Name) == false { return fmt.Errorf("invalid name %q", f.Name) }
} Try / catch
zipBytes, err := createZipFileInMemory(files)
if err != nil {
if strings.Contains(err.Error(), "failed to close zip writer") {
return fmt.Errorf("export archive could not be finalized; underlying storage error: %w", err)
}
return err
}
// serve zipBytes... Prevention
- Call zipWriter.Close exactly once and check its error before returning bytes
- Never defer both Close and a manual Close on the same writer
- After changing the sink from bytes.Buffer, handle disk-full (ENOSPC) on Close
- In tests, reopen the produced bytes with zip.NewReader to verify archive integrity
When it happens
Trigger: Underlying writer I/O failure during central-directory write (real for file/network sinks, not for bytes.Buffer); calling Close twice or after the writer was handed to another goroutine; a prior entry writer left in a broken state.
Common situations: Disk full when writing export ZIPs to disk; interrupted container filesystem; refactors that defer close twice; returning the buffer before Close completes, producing a truncated/unopenable ZIP that users report as 'corrupt export'.
Related errors
- failed to create zip entry for %s: %v
- failed to write content to zip for %s: %v
- disk quota exceeded
- illegal file path:
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/220d2a772caa61bc.
Report an issue: GitHub.