{"record":{"id":"6a6ac742e198eae7","repo":"passbolt/passbolt_api","slug":"e-getmessage-avatar-stream-read-failure","errorCode":null,"errorMessage":"$e->getMessage() (avatar stream read failure)","messagePattern":"\\$e->getMessage\\(\\) \\(avatar stream read failure\\)","errorType":"http","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Controller/Avatars/AvatarsViewController.php","lineNumber":57,"sourceCode":"     * @return \\Cake\\Http\\Response\n     */\n    public function view(\n        string $id,\n        string $format,\n        FilesystemAdapter $filesystemAdapter\n    ): Response {\n        $formatIsValid = $this->validateImageFormat($format);\n        if ($formatIsValid === false) {\n            $id = null;\n        }\n\n        $service = new AvatarsCacheService($filesystemAdapter);\n\n        try {\n            $stream = $service->readSteamFromId($id, $format);\n        } catch (Throwable $e) {\n            Log::error($e->getMessage());\n            throw new NotFoundException($e->getMessage());\n        }\n\n        return $this->getResponse()\n            ->withType('jpg')\n            ->withBody($stream);\n    }\n\n    /**\n     * Checks if the format provided is medium or small,\n     * and that the extension is .jpg\n     *\n     * @param string $format Image format provided in the requested url.\n     * @return bool\n     */\n    protected function validateImageFormat(string $format): bool\n    {\n        $validFormats = AvatarHelper::getValidImageFormats();\n","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/src/Controller/Avatars/AvatarsViewController.php#L39-L75","documentation":"The avatar view endpoint fails to read the avatar image stream from cache/filesystem storage. AvatarsViewController catches any Throwable from AvatarsCacheService::readSteamFromId(), logs it, and rethrows it as a NotFoundException so the HTTP response is a 404 instead of a 500 with a leaked internal message. Any storage backend misconfiguration, missing avatar file, or stream read failure surfaces as this error.","triggerScenarios":"GET /avatars/view/<id>.jpg (or <id>.png) when the FilesystemAdapter cannot locate or read the avatar file for the given id/format; the underlying adapter throws (e.g. league/flysystem unableToReadFile) and readSteamFromId propagates it.","commonSituations":"Avatar cache directory not writable or missing on the server; avatar record/file deleted or never generated; wrong storage adapter config (path, permissions, S3 credentials) after environment migration; user id passed has no avatar; deployment wiped webroot/cache between releases.","solutions":["Check server logs for the underlying Throwable message logged by Log::error() — it names the real storage failure.","Verify the filesystem adapter configuration (base path/credentials) and that the cache directory exists and is writable by the web server user.","Confirm the avatar record and file exist for the requested id; re-upload the avatar or clear/rebuild the avatar cache.","If 404 is expected (no avatar), ensure clients fall back to a default avatar image instead of treating it as a bug."],"exampleFix":"// before — raw storage exception surfaces as misleading 404\ntry {\n    $stream = $service->readSteamFromId($id, $format);\n} catch (Throwable $e) {\n    Log::error($e->getMessage());\n    throw new NotFoundException($e->getMessage());\n}\n// after — fall back to a default avatar stream so storage hiccups don't 404\ntry {\n    $stream = $service->readSteamFromId($id, $format);\n} catch (Throwable $e) {\n    Log::error('Avatar read failed: ' . $e->getMessage());\n    $stream = $service->readDefaultAvatar($format); // or return a placeholder image response\n}","handlingStrategy":"fallback","validationCode":"// client-side pre-check\nif (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(userId)) {\n  throw new Error('cannot request avatar: invalid user id');\n}","typeGuard":"const isUuid = (v) => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);","tryCatchPattern":"try {\n  const res = await fetch(`/avatars/view/${userId}.jpg`);\n  if (res.status === 404) return DEFAULT_AVATAR_URL; // NotFoundException expected when avatar missing/unreadable\n  if (!res.ok) throw new Error(`avatar fetch failed: ${res.status}`);\n  return URL.createObjectURL(await res.blob());\n} catch (e) {\n  return DEFAULT_AVATAR_URL;\n}","preventionTips":["Always render a default/placeholder avatar on 404 instead of treating it as a hard failure.","Verify the avatar cache directory exists and is writable after each deployment.","Monitor server logs for the underlying Log::error() message to catch storage misconfigurations early.","Validate the format path segment (.jpg/.png) against supported formats before requesting."],"tags":["avatars","file-storage","http-404","flysystem"],"backgroundTag":"file-read-failed","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}