{"record":{"id":"8d68be2dc0d9b156","repo":"getgrav/grav","slug":"failed-to-save-file-s-s","errorCode":null,"errorMessage":"Failed to save file '%s': %s","messagePattern":"Failed to save file '(.+?)': (.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Framework/File/DataFile.php","lineNumber":69,"sourceCode":"\n            return $this->formatter->decode($raw);\n        } catch (RuntimeException $e) {\n            throw new RuntimeException(sprintf(\"Failed to load file '%s': %s\", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e);\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     * @see FileInterface::save()\n     */\n    public function save($data): void\n    {\n        if (is_string($data)) {\n            // Make sure that the string is valid data.\n            try {\n                $this->formatter->decode($data);\n            } catch (RuntimeException $e) {\n                throw new RuntimeException(sprintf(\"Failed to save file '%s': %s\", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e);\n            }\n            $encoded = $data;\n        } else {\n            $encoded = $this->formatter->encode($data);\n        }\n\n        parent::save($encoded);\n    }\n}\n","sourceCodeStart":51,"sourceCodeEnd":79,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Framework/File/DataFile.php#L51-L79","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Pass arrays and let the formatter encode them — the intended API contract.","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.","Fix the syntax error named in the message tail.","Strip BOM and surrounding whitespace from external strings before passing them in."],"exampleFix":"// before\n$file->save('{ \"a\": 1,, }');\n\n// after (preferred: arrays)\n$file->save(['a' => 1]);\n\n// or validated raw string\njson_decode($raw); \nif (json_last_error() !== JSON_ERROR_NONE) { throw new InvalidArgumentException('bad payload'); }\n$file->save($raw);","handlingStrategy":"validation","validationCode":"if (is_string($data)) {\n    // same check DataFile performs: verify the string parses before save()\n    json_decode($data);\n    if (json_last_error() !== JSON_ERROR_NONE) {\n        throw new \\InvalidArgumentException('payload is not valid JSON');\n    }\n}\n$dataFile->save($data);","typeGuard":"function isFormattablePayload(mixed $data): bool\n{\n    return is_array($data) || is_string($data);\n}","tryCatchPattern":"try {\n    $dataFile->save($data);\n} catch (\\RuntimeException $e) {\n    // string failed the round-trip check; file untouched — fix payload and retry\n    $log->error($e->getMessage());\n}","preventionTips":["Prefer passing arrays; let the formatter own serialization.","Validate externally sourced strings with the same format's parser before saving.","Never assemble JSON/YAML/INI text by concatenation."],"tags":["php","grav","validation","data-file","serialization","file-save"],"backgroundTag":"file-parse-failed","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}