getgrav/grav · error · RuntimeException

Failed to save file '%s': %s

Error message

Failed to save file '%s': %s

What it means

DataFile::save() accepts either an array (which the configured formatter encodes) or a raw string. A string is trusted only after a round-trip check: the formatter decodes it, and any RuntimeException (invalid syntax for the target format) aborts the save with this message (path + formatter reason). The existing file is left untouched because encoding happens before any write.

Source

Thrown at system/src/Grav/Framework/File/DataFile.php:69

            return $this->formatter->decode($raw);
        } catch (RuntimeException $e) {
            throw new RuntimeException(sprintf("Failed to load file '%s': %s", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e);
        }
    }

    /**
     * {@inheritdoc}
     * @see FileInterface::save()
     */
    public function save($data): void
    {
        if (is_string($data)) {
            // Make sure that the string is valid data.
            try {
                $this->formatter->decode($data);
            } catch (RuntimeException $e) {
                throw new RuntimeException(sprintf("Failed to save file '%s': %s", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e);
            }
            $encoded = $data;
        } else {
            $encoded = $this->formatter->encode($data);
        }

        parent::save($encoded);
    }
}

View on GitHub (pinned to 6040efed04)

Solutions

  1. Pass arrays and let the formatter encode them — the intended API contract.
  2. If a raw string must be saved, validate it first: json_decode($s) with json_last_error(), or the formatter's decode() in a try/catch.
  3. Fix the syntax error named in the message tail.
  4. Strip BOM and surrounding whitespace from external strings before passing them in.

Example fix

// before
$file->save('{ "a": 1,, }');

// after (preferred: arrays)
$file->save(['a' => 1]);

// or validated raw string
json_decode($raw); 
if (json_last_error() !== JSON_ERROR_NONE) { throw new InvalidArgumentException('bad payload'); }
$file->save($raw);
Defensive patterns

Strategy: validation

Validate before calling

if (is_string($data)) {
    // same check DataFile performs: verify the string parses before save()
    json_decode($data);
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new \InvalidArgumentException('payload is not valid JSON');
    }
}
$dataFile->save($data);

Type guard

function isFormattablePayload(mixed $data): bool
{
    return is_array($data) || is_string($data);
}

Try / catch

try {
    $dataFile->save($data);
} catch (\RuntimeException $e) {
    // string failed the round-trip check; file untouched — fix payload and retry
    $log->error($e->getMessage());
}

Prevention

When it happens

Trigger: Calling save("not valid { json") on a .json DataFile; an INI or CSV string whose syntax the corresponding formatter rejects; strings built by concatenation or templates that contain stray quotes, BOMs or half-written fragments.

Common situations: Plugins composing JSON/YAML text by hand instead of passing arrays; piping downloaded or user-submitted strings into save(); mixing encodings that make the formatter's parser fail.

Related errors


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