getgrav/grav · error · RuntimeException

CSV header missing

Error message

CSV header missing

What it means

CsvFormatter::decode() treats the first line as a mandatory header row: after splitting, array_shift removes it and throws when it is falsy. Concretely the payload's first line is empty — the data is an empty string, contains only whitespace/newlines, or begins with blank line(s) — so there are no field names to map columns against.

Source

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

    /**
     * @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) {
                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);

View on GitHub (pinned to 6040efed04)

Solutions

  1. Guard empty input before decoding: if (trim($data) === '') treat as empty dataset ([] or defaults), not an error.
  2. Fix the source to emit a header row (re-export with headers enabled).
  3. Strip leading blank lines when they are expected: $data = ltrim($data, "\r\n").
  4. If the schema is known, prepend the header yourself: $data = implode(',', $fields) . "\n" . $data;

Example fix

// before
$list = $formatter->decode($raw);

// after
$raw = is_string($raw) ? ltrim($raw, "\r\n") : '';
$list = ($raw === '') ? [] : $formatter->decode($raw);
Defensive patterns

Strategy: validation

Validate before calling

if (!is_string($data) || trim($data) === '') {
    // no header possible: treat as empty dataset instead of calling decode()
    return [];
}

Try / catch

try {
    $list = $formatter->decode($data);
} catch (\RuntimeException $e) {
    if (str_contains($e->getMessage(), 'CSV header missing')) {
        $list = []; // empty input is not an error for the caller
    } else {
        throw $e;
    }
}

Prevention

When it happens

Trigger: decode(''), decode("\n\n1,2"); a CSV export written without its header row; a file truncated so the first line is blank; uploads that are empty before validation.

Common situations: Export tools configured to omit headers; plugins feeding user uploads directly into the formatter; empty seed files shipped with a plugin; files saved with a stray leading newline.

Related errors


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