Billionmail/BillionMail · error
failed to create zip entry for
Error message
failed to create zip entry for %s: %v
What it means
createZipFileInMemory builds a ZIP in memory using archive/zip. zipWriter.Create(name) returns a writer for a new entry; it fails when the archive writer is in a bad state (e.g. it was already closed) or the entry name is invalid for the ZIP format. This wrapper adds the offending file name to the error.
Solutions
- Sanitize ExportFile.Name before building the ZIP (strip control characters, ensure valid UTF-8, reasonable length).
- Use errors.Is/As or %w wrapping to preserve the underlying cause for diagnostics.
- Ensure zipWriter.Close() is called exactly once, after all entries are written.
- For special characters in group names, set zipWriter's flags or normalize names to ASCII.
- If creating entries concurrently, serialize the calls — archive/zip.Writer is not safe for concurrent use.
Example fix
// before
zipEntry, err := zipWriter.Create(file.Name)
if err != nil {
return nil, fmt.Errorf("failed to create zip entry for %s: %v", file.Name, err)
}
// after
safeName := strings.Map(func(r rune) rune {
if r < 32 || r == 127 {
return -1
}
return r
}, file.Name)
zipEntry, err := zipWriter.Create(safeName)
if err != nil {
return nil, fmt.Errorf("failed to create zip entry for %q: %w", safeName, err)
} Defensive patterns
Strategy: validation
Validate before calling
func safeZipName(name string) string {
name = strings.TrimSpace(name)
if !utf8.ValidString(name) { name = strings.ToValidUTF8(name, "_") }
name = strings.Map(func(r rune) rune { if r < 32 || r == 127 { return -1 }; return r }, name)
if name == "" { name = "export.csv" }
return name
}
// apply to each ExportFile.Name before createZipFileInMemory Try / catch
out, err := createZipFileInMemory(files)
if err != nil {
var zerr *zip.Error
if errors.As(err, &zerr) {
return fmt.Errorf("zip export failed (%v); check file names", zerr)
}
return fmt.Errorf("export failed: %w", err)
} Prevention
- Sanitize entry names: no control chars, valid UTF-8, reasonable length
- Never reuse or close the zip.Writer between Create calls
- Serialize zip.Writer usage — it is not concurrency-safe
- Add a test that exports group names containing unicode/special characters
When it happens
Trigger: Calling createZipFileInMemory with a file whose Name cannot be used as a ZIP entry name (e.g. invalid UTF-8 in the name), or reusing/closing the zip.Writer across calls and then creating another entry; in practice with this code, a fresh zip.Writer only fails on malformed entry names.
Common situations: Group names with control or non-UTF-8 characters flowing into ExportFile.Name; code refactors that close the writer per entry; concurrent use of a single zip.Writer (not goroutine-safe).
Related errors
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/c6537b09443088c5.
Report an issue: GitHub.
Appendix: source
Thrown at core/internal/controller/contact/contact_v1_export_contacts.go:217
}
// ExportFile Export file information
type ExportFile struct {
Name string // File name
Content string // File content
Contacts int // Contact count
}
// createZipFileInMemory creates a ZIP file in memory
func createZipFileInMemory(files []ExportFile) ([]byte, error) {
var buf bytes.Buffer
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 formatView on GitHub (pinned to fc36c76c05)