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

  1. With the current in-memory buffer this should never fire; if it does after changing the sink, verify disk space and file permissions.
  2. Call zipWriter.Close exactly once, before returning buf.Bytes(); never rely on defer alone if the return value depends on Close.
  3. Wrap with %w to let callers distinguish close errors from entry errors.
  4. If streaming to HTTP, flush the response writer before Close and handle client-abort errors.
  5. 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

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


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/220d2a772caa61bc. Report an issue: GitHub.