getgrav/grav · error · RuntimeException

Decoding CSV failed

Error message

Decoding CSV failed

What it means

CsvFormatter::decode() splits the payload into lines with preg_split('/\r\n|\r|\n/', $data) and throws this only when preg_split itself fails (returns false) — a PCRE engine failure, not a CSV shape problem. The classic cause is exhausting pcre.backtrack_limit on very large input; malformed-but-splittable data produces the sibling 'CSV header missing' / 'Badly formatted CSV line' errors instead.

Source

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

        foreach ($data as $row) {
            $string .= $this->encodeLine($row, $delimiter);
        }

        return $string;
    }

    /**
     * @param string $data
     * @param string|null $delimiter
     * @return array
     * @see FileFormatterInterface::decode()
     */
    public function decode($data, $delimiter = null): array
    {
        $delimiter ??= $this->getDelimiter();
        $lines = preg_split('/\r\n|\r|\n/', $data);
        if ($lines === false) {
            throw new RuntimeException('Decoding CSV failed');
        }

        // Get the field names
        $headerStr = array_shift($lines);
        if (!$headerStr) {
            throw new RuntimeException('CSV header missing');
        }

        $header = str_getcsv($headerStr, $delimiter);

        // Allow for replacing a null string with null/empty value
        $null_replace = $this->getConfig('null');

        // Get the data
        $list = [];
        $line = null;
        try {
            foreach ($lines as $line) {

View on GitHub (pinned to 6040efed04)

Solutions

  1. Raise the limit for the import: ini_set('pcre.backtrack_limit', '10000000');.
  2. Stream instead of loading whole: SplFileObject with READ_CSV flag or fgetcsv() row by row.
  3. Chunk the input and decode pieces, preserving the header row for each chunk.
  4. Diagnose with a manual split: preg_split(...) then preg_last_error() to confirm PREG_BACKTRACK_LIMIT_ERROR.

Example fix

// before
$rows = $csvFormatter->decode(file_get_contents('big.csv'));

// after: stream rows, no whole-file regex split
$file = new \SplFileObject('big.csv');
$file->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY);
$rows = [];
foreach ($file as $row) {
    if ($row !== false && $row[0] !== null) { $rows[] = $row; }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Probe the engine before decode(): same split the formatter performs
$lines = @preg_split('/\r\n|\r|\n/', $data);
if ($lines === false) {
    $err = preg_last_error(); // PREG_BACKTRACK_LIMIT_ERROR etc.
    // chunk the data, raise pcre.backtrack_limit, or use a streaming reader
}

Try / catch

try {
    $rows = $csvFormatter->decode($data);
} catch (\RuntimeException $e) {
    // fall back to row-by-row streaming (SplFileObject::READ_CSV)
    $rows = streamCsv($path);
}

Prevention

When it happens

Trigger: Decoding a multi-megabyte CSV (or one enormous single line) through CsvFormatter when pcre.backtrack_limit (default ~1M) is exceeded; constrained runtimes where the PCRE engine errors during the split.

Common situations: Plugins importing large data exports via the formatter instead of a streaming reader; shared hosting with low PCRE limits; PHP-FPM pools with restrictive ini settings.

Related errors


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