getgrav/grav · error · RuntimeException

Badly formatted CSV line

Error message

Badly formatted CSV line: {line}

What it means

CsvFormatter::decode() parses each line with str_getcsv() and merges it with the header via array_combine($header, $csv_line) inside a try/catch; any Exception thrown while processing a line is rethrown as 'Badly formatted CSV line: <line>'. The dominant cause is a field-count mismatch between a row and the header — one side of array_combine longer than the other — typically from unquoted delimiters, missing trailing delimiters, or the wrong delimiter being used.

Solutions

  1. Inspect the line named in the message and balance its field count against the header.
  2. Match the delimiter: pass it explicitly decode($data, ';') or configure the CsvFormatter — a systematic wrong delimiter produces this error on every row.
  3. Re-export with proper quoting so values containing the delimiter are enclosed in quotes.
  4. Pre-validate rows before decoding: count(str_getcsv($line, $del)) must equal count($header); skip or report offenders.

Example fix

// before
$list = $formatter->decode($raw); // Badly formatted CSV line: 1;2;3;4

// after: pass the actual delimiter
$list = $formatter->decode($raw, ';');

// or guard rows against the header width
$header = str_getcsv($headerLine, $del);
$clean = array_filter($lines, fn($l) => count(str_getcsv($l, $del)) === count($header));
Defensive patterns

Strategy: validation

Validate before calling

// Verify every row's width matches the header before decode()
$lines = preg_split('/\r\n|\r|\n/', trim($data));
$header = str_getcsv((string) array_shift($lines), $del);
foreach ($lines as $l) {
    if ($l !== '' && count(str_getcsv($l, $del)) !== count($header)) {
        // report/skip this row now instead of failing the whole decode
    }
}

Try / catch

try {
    $list = $formatter->decode($data);
} catch (\RuntimeException $e) {
    if (preg_match('/Badly formatted CSV line/', $e->getMessage())) {
        // row-level data error: log and continue with remaining rows if acceptable
    }
}

Prevention

When it happens

Trigger: A data row with more fields than the header (unquoted delimiter inside a value) or fewer (missing trailing comma); a delimiter mismatch — the file is semicolon/tab separated while the formatter uses commas, so counts never line up; rows of a different width appended from another source.

Common situations: Spreadsheet exports with unquoted commas in text fields; concatenating CSVs that share a schema prefix but drift later; European locale exports (semicolon delimiter) read with the default comma config; hand-edited rows.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/d10b4b1ba600dfde. Report an issue: GitHub.

Appendix: source

Thrown at system/src/Grav/Framework/File/Formatter/CsvFormatter.php:124

        // Get the data
        $list = [];
        $line = null;
        try {
            foreach ($lines as $line) {
                if (!empty($line)) {
                    $csv_line = str_getcsv($line, $delimiter);

                    if ($null_replace) {
                        array_walk($csv_line, static function (&$el) use ($null_replace) {
                            $el = str_replace($null_replace, "\0", $el);
                        });
                    }

                    $list[] = array_combine($header, $csv_line);
                }
            }
        } catch (Exception) {
            throw new RuntimeException('Badly formatted CSV line: ' . $line);
        }

        return $list;
    }

    /**
     * @param array $line
     * @param string $delimiter
     * @return string
     */
    protected function encodeLine(array $line, string $delimiter): string
    {
        foreach ($line as $key => &$value) {
            // Oops, we need to convert the line to a string.
            if (!is_scalar($value)) {
                if (is_array($value) || $value instanceof JsonSerializable || $value instanceof stdClass) {
                    $value = json_encode($value);
                } elseif (is_object($value)) {

View on GitHub (pinned to 6040efed04)