{"record":{"id":"dcb9e7d12f0b4220","repo":"w7corp/easywechat","slug":"cannot-save-response-to-s-s","errorCode":null,"errorMessage":"Cannot save response to %s: %s","messagePattern":"Cannot save response to (.+?): (.+?)","errorType":"exception","errorClass":"BadResponseException","httpStatus":null,"severity":"error","filePath":"src/Kernel/HttpClient/Response.php","lineNumber":193,"sourceCode":"        $body = $this->response instanceof StreamableInterface ? $this->toStream(false) : StreamWrapper::createResource($this->response);\n        $body = $streamFactory->createStreamFromResource($body);\n\n        if ($body->isSeekable()) {\n            $body->seek(0);\n        }\n\n        return $psrResponse->withBody($body);\n    }\n\n    /**\n     * @throws BadResponseException\n     */\n    public function saveAs(string $filename): string\n    {\n        try {\n            file_put_contents($filename, $this->response->getContent(true));\n        } catch (Throwable $e) {\n            throw new BadResponseException(sprintf(\n                'Cannot save response to %s: %s',\n                $filename,\n                $this->response->getContent(false)\n            ), $e->getCode(), $e);\n        }\n\n        return '';\n    }\n\n    public function offsetExists(mixed $offset): bool\n    {\n        return array_key_exists($offset, $this->toArray());\n    }\n\n    public function offsetGet(mixed $offset): mixed\n    {\n        return $this->toArray()[$offset] ?? null;\n    }","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/w7corp/easywechat/blob/f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8/src/Kernel/HttpClient/Response.php#L175-L211","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n$path = '/var/www/downloads/'.$mediaId.'.jpg';\n$response->saveAs($path); // may throw\n\n// after\n$dir = dirname($path);\nif (!is_dir($dir)) {\n    mkdir($dir, 0755, true);\n}\nif (!is_writable($dir)) {\n    throw new RuntimeException(\"Download dir not writable: {$dir}\");\n}\n$response->saveAs($path);","handlingStrategy":"validation","validationCode":"$dir = dirname($filename);\nif (! is_dir($dir) && ! mkdir($dir, 0755, true) && ! is_dir($dir)) {\n    throw new RuntimeException(\"Cannot create download directory: {$dir}\");\n}\nif (! is_writable($dir)) {\n    throw new RuntimeException(\"Download directory not writable: {$dir}\");\n}\n$response->saveAs($filename);","typeGuard":null,"tryCatchPattern":"try {\n    $response->saveAs($filename);\n} catch (\\Symfony\\Component\\HttpClient\\Exception\\BadResponseException $e) {\n    $reason = $e->getPrevious()?->getMessage() ?? $e->getMessage();\n    log_download_failure($filename, $reason);\n    throw new RuntimeException(\"Failed to save {$filename}: {$reason}\", 0, $e);\n}","preventionTips":["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"],"tags":["php","easywechat","file-download","filesystem","permissions"],"backgroundTag":"file-write-permission-denied","analyzedSha":"f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8","analyzedAt":"2026-08-21T05:29:19.565Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}