passbolt/passbolt_api · error · NotFoundException

$e->getMessage() (avatar stream read failure)

Error message

$e->getMessage() (avatar stream read failure)

What it means

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.

Solutions

  1. Check server logs for the underlying Throwable message logged by Log::error() — it names the real storage failure.
  2. Verify the filesystem adapter configuration (base path/credentials) and that the cache directory exists and is writable by the web server user.
  3. Confirm the avatar record and file exist for the requested id; re-upload the avatar or clear/rebuild the avatar cache.
  4. If 404 is expected (no avatar), ensure clients fall back to a default avatar image instead of treating it as a bug.

Example fix

// before — raw storage exception surfaces as misleading 404
try {
    $stream = $service->readSteamFromId($id, $format);
} catch (Throwable $e) {
    Log::error($e->getMessage());
    throw new NotFoundException($e->getMessage());
}
// after — fall back to a default avatar stream so storage hiccups don't 404
try {
    $stream = $service->readSteamFromId($id, $format);
} catch (Throwable $e) {
    Log::error('Avatar read failed: ' . $e->getMessage());
    $stream = $service->readDefaultAvatar($format); // or return a placeholder image response
}
Defensive patterns

Strategy: fallback

Validate before calling

// client-side pre-check
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(userId)) {
  throw new Error('cannot request avatar: invalid user id');
}

Type guard

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);

Try / catch

try {
  const res = await fetch(`/avatars/view/${userId}.jpg`);
  if (res.status === 404) return DEFAULT_AVATAR_URL; // NotFoundException expected when avatar missing/unreadable
  if (!res.ok) throw new Error(`avatar fetch failed: ${res.status}`);
  return URL.createObjectURL(await res.blob());
} catch (e) {
  return DEFAULT_AVATAR_URL;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17). Data as JSON: /api/errors/6a6ac742e198eae7. Report an issue: GitHub.

Appendix: source

Thrown at src/Controller/Avatars/AvatarsViewController.php:57

     * @return \Cake\Http\Response
     */
    public function view(
        string $id,
        string $format,
        FilesystemAdapter $filesystemAdapter
    ): Response {
        $formatIsValid = $this->validateImageFormat($format);
        if ($formatIsValid === false) {
            $id = null;
        }

        $service = new AvatarsCacheService($filesystemAdapter);

        try {
            $stream = $service->readSteamFromId($id, $format);
        } catch (Throwable $e) {
            Log::error($e->getMessage());
            throw new NotFoundException($e->getMessage());
        }

        return $this->getResponse()
            ->withType('jpg')
            ->withBody($stream);
    }

    /**
     * Checks if the format provided is medium or small,
     * and that the extension is .jpg
     *
     * @param string $format Image format provided in the requested url.
     * @return bool
     */
    protected function validateImageFormat(string $format): bool
    {
        $validFormats = AvatarHelper::getValidImageFormats();

View on GitHub (pinned to 31c1bbc10f)