w7corp/easywechat · error · BadResponseException

Cannot save response to %s: %s

Error message

Cannot save response to %s: %s

What it means

Response::saveAs() writes the body with file_put_contents and converts any write failure into BadResponseException('Cannot save response to <filename>: <content>'). The <content> slot is the response body itself (useful for tiny error payloads), while the real OS-level reason (permission denied, no such directory, disk full) is in the chained previous exception from the PHP warning.

Source

Thrown at src/Kernel/HttpClient/Response.php:193

        $body = $this->response instanceof StreamableInterface ? $this->toStream(false) : StreamWrapper::createResource($this->response);
        $body = $streamFactory->createStreamFromResource($body);

        if ($body->isSeekable()) {
            $body->seek(0);
        }

        return $psrResponse->withBody($body);
    }

    /**
     * @throws BadResponseException
     */
    public function saveAs(string $filename): string
    {
        try {
            file_put_contents($filename, $this->response->getContent(true));
        } catch (Throwable $e) {
            throw new BadResponseException(sprintf(
                'Cannot save response to %s: %s',
                $filename,
                $this->response->getContent(false)
            ), $e->getCode(), $e);
        }

        return '';
    }

    public function offsetExists(mixed $offset): bool
    {
        return array_key_exists($offset, $this->toArray());
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->toArray()[$offset] ?? null;
    }

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. Ensure the directory exists and is writable: mkdir($dir, 0755, true) plus is_writable($dir) before the call
  2. Check the previous exception ($e->getPrevious()->getMessage()) for the real PHP warning (e.g. 'failed to open stream: Permission denied')
  3. Write to a framework storage path or /tmp then move, rather than directly to a served public directory

Example fix

// before
$path = '/var/www/downloads/'.$mediaId.'.jpg';
$response->saveAs($path); // may throw

// after
$dir = dirname($path);
if (!is_dir($dir)) {
    mkdir($dir, 0755, true);
}
if (!is_writable($dir)) {
    throw new RuntimeException("Download dir not writable: {$dir}");
}
$response->saveAs($path);
Defensive patterns

Strategy: validation

Validate before calling

$dir = dirname($filename);
if (! is_dir($dir) && ! mkdir($dir, 0755, true) && ! is_dir($dir)) {
    throw new RuntimeException("Cannot create download directory: {$dir}");
}
if (! is_writable($dir)) {
    throw new RuntimeException("Download directory not writable: {$dir}");
}
$response->saveAs($filename);

Try / catch

try {
    $response->saveAs($filename);
} catch (\Symfony\Component\HttpClient\Exception\BadResponseException $e) {
    $reason = $e->getPrevious()?->getMessage() ?? $e->getMessage();
    log_download_failure($filename, $reason);
    throw new RuntimeException("Failed to save {$filename}: {$reason}", 0, $e);
}

Prevention

When it happens

Trigger: Downloading media files (->saveAs('/var/www/downloads/media.jpg')) when the target directory does not exist, is not writable by the PHP-FPM user, the disk is full, or the path falls outside open_basedir.

Common situations: Hardcoded absolute paths that exist in dev but not prod, web-server user (www-data) lacking write permission on the storage directory, containers with ephemeral/full disks.

Related errors


AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21). Data as JSON: /api/errors/dcb9e7d12f0b4220. Report an issue: GitHub.