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
- Ensure the directory exists and is writable: mkdir($dir, 0755, true) plus is_writable($dir) before the call
- Check the previous exception ($e->getPrevious()->getMessage()) for the real PHP warning (e.g. 'failed to open stream: Permission denied')
- 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
- Create and permission the download directory at deploy time, not at request time
- Check is_writable(dirname()) before saveAs() to convert a confusing error into a clear one
- Remember the exception message embeds the response body; read getPrevious() for the real OS error
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.