guzzle/guzzle · error · \RuntimeException

Unable to save file %s

Error message

Unable to save file %s

What it means

Thrown by FileCookieJar::save() when file_put_contents() returns false, meaning the cookie JSON could not be written. The %s is the escaped filename (DiagnosticValue::escape is used to keep control bytes out of the message). This is an I/O failure: the path is unwritable, the directory does not exist, permissions are denied, or the disk is full.

Source

Thrown at src/Cookie/FileCookieJar.php:110

    {
        $json = [];
        /** @var SetCookie $cookie */
        foreach ($this as $cookie) {
            if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
                $data = $cookie->toArray();
                $data['HostOnly'] = $cookie->getHostOnly();
                $json[] = $data;
            }
        }

        try {
            $jsonStr = \json_encode($json, \JSON_HEX_TAG | \JSON_THROW_ON_ERROR);
        } catch (\JsonException $e) {
            throw new \RuntimeException('Unable to encode cookie data', 0, $e);
        }

        if (false === \file_put_contents($filename, $jsonStr, \LOCK_EX)) {
            throw new \RuntimeException(\sprintf('Unable to save file %s', DiagnosticValue::escape($filename)));
        }

        // Best-effort: restrict the cookie file to the owner so persisted
        // cookies are not world-readable.
        @\chmod($filename, 0600);
    }

    /**
     * Load cookies from a JSON formatted file.
     *
     * Old cookies are kept unless overwritten by newly loaded ones.
     * Cookie records are constructed before any are passed to setCookie().
     *
     * @param string $filename Cookie file to load.
     *
     * @throws \RuntimeException if the file cannot be loaded or is invalid
     */
    public function load(string $filename): void

View on GitHub (pinned to 9b200fc580)

Solutions

  1. Verify the directory exists and is writable by the PHP process user: is_dir(dirname($path)) && is_writable(dirname($path)).
  2. Use an absolute path under a known-writable directory (e.g. sys_get_temp_dir() or a dedicated var/ dir).
  3. Pre-create the file and chmod 0600 it; ensure the web user owns it.
  4. Catch \RuntimeException in __destruct context by calling save() explicitly in a try/catch (the destructor itself cannot meaningfully throw to the caller).

Example fix

// before
$jar = new FileCookieJar('/var/lock/cookies.json'); // not writable by app

// after
$path = dirname(__DIR__).'/var/cookies.json';
@touch($path); @chmod($path, 0600);
$jar = new FileCookieJar($path);
Defensive patterns

Strategy: validation

Validate before calling

$dir = dirname($path);
if (!is_dir($dir) || !is_writable($dir)) {
    throw new RuntimeException("Cookie dir not writable: $dir");
}
@touch($path); @chmod($path, 0600);
$jar = new FileCookieJar($path);

Type guard

function cookieFileIsWritable(string $path): bool
{
    $dir = dirname($path);
    return is_dir($dir) && is_writable($dir)
        && (!file_exists($path) || is_writable($path));
}

Try / catch

try {
    $jar->save($path);
} catch (\RuntimeException $e) {
    if (str_starts_with($e->getMessage(), 'Unable to save file')) {
        // fall back to a temp dir or in-memory jar
    }
}

Prevention

When it happens

Trigger: Constructing `new FileCookieJar('/nonexistent/cookies.json')` then letting the destructor fire save(); pointing the jar at a directory that is not writable; running the script as a user without write permission to the file; SELinux/AppArmor denying the write; read-only filesystem.

Common situations: Deploying to a container where the target path is a read-only mounted volume; web user (www-data) lacking write permission on the cookies file; passing a relative path that resolves to an unexpected cwd; disk-full conditions.

Related errors


AI-assisted analysis of guzzle/guzzle@9b200fc580 (2026-08-04). Data as JSON: /data/errors/1a2ea1249921e5f2.json. Report an issue: GitHub.