Billionmail/BillionMail · error

failed to read CSV headers: %v

Error message

failed to read CSV headers: %v

What it means

parseCSVContent wraps the error returned by encoding/csv Reader.Read() when it fails to read the first (header) record of an uploaded contacts CSV. LazyQuotes and TrimLeadingSpace are enabled, so this fires only on structural CSV problems, not sloppy quoting. The underlying cause is preserved via %v in the wrapped message.

Source

Thrown at core/internal/controller/contact/contact_v1_import_contacts.go:108

		}
	}

	g.Log().Debug(ctx, "Final parsed attributes: %v", stringAttribs)
	return stringAttribs, nil
}

// parseCSVContent Parse CSV content
func parseCSVContent(ctx context.Context, content string) ([]*entity.Contact, error) {
	var contacts []*entity.Contact
	reader := csv.NewReader(bytes.NewReader([]byte(content)))
	reader.FieldsPerRecord = -1
	reader.LazyQuotes = true
	reader.TrimLeadingSpace = true

	// Read headers
	headers, err := reader.Read()
	if err != nil {
		return nil, fmt.Errorf("failed to read CSV headers: %v", err)
	}

	// Find the index of the required columns
	columnIndexes := make(map[string]int)
	for i, header := range headers {
		header = strings.ToLower(strings.TrimSpace(header))
		columnIndexes[header] = i
	}

	// Check the required columns
	if _, ok := columnIndexes["email"]; !ok {
		return nil, fmt.Errorf("required column 'email' not found")
	}

	// Process data rows
	for {
		record, err := reader.Read()
		if err == io.EOF {

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Open the uploaded CSV in a spreadsheet/text editor and fix the header row: equal number of comma-separated columns on every row.
  2. Ensure the file is valid UTF-8 without a stray BOM and uses consistent CRLF/LF line endings.
  3. If rows legitimately have different lengths, set reader.FieldsPerRecord = -1 before Read() to disable the field-count check.
  4. Verify the uploaded file is a real CSV, not an xlsx or HTML error page saved with .csv extension.

Example fix

// before
reader.LazyQuotes = true
reader.TrimLeadingSpace = true
headers, err := reader.Read()
// after
reader.LazyQuotes = true
reader.TrimLeadingSpace = true
reader.FieldsPerRecord = -1 // tolerate ragged rows
headers, err := reader.Read()
Defensive patterns

Strategy: validation

Validate before calling

data, err := os.ReadFile(path)
if err != nil { return err }
if !utf8.Valid(data) { return errors.New("CSV is not valid UTF-8") }
r := csv.NewReader(bytes.NewReader(data))
r.FieldsPerRecord = -1
if _, err := r.Read(); err != nil {
	return fmt.Errorf("invalid CSV header row: %w", err)
}

Try / catch

headers, err := parseCSVContent(ctx, file)
if err != nil {
	var csvErr *csv.ParseError
	if errors.As(err, &csvErr) {
		return fmt.Errorf("CSV malformed at line %d: %w", csvErr.Line, err)
	}
	return err
}

Prevention

When it happens

Trigger: reader.Read() on the header line returns a non-EOF error, e.g. a field count mismatch against the first record when useFieldsPerRecord defaults (headers row has 3 columns, later rows have 5), or a bare-quote/rune-reading error that LazyQuotes did not suppress.

Common situations: Users export contacts from Excel/Google Sheets with ragged rows; CSVs with embedded quotes or non-UTF8 bytes; empty or truncated uploads that produce a malformed header; BOM or CRLF quirks shifting field parsing.

Related errors


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