getgrav/grav · warning · RuntimeException

Cannot read file

Error message

Cannot read file

What it means

LogViewer reads a log file backwards in buffer-sized chunks to extract the last N lines; if fread() ever returns false mid-loop (line 81) it throws 'Cannot read file'. fread only fails like this when the open handle became unreadable or the read was invalid — typically the file was deleted/replaced under the handle, permissions changed, or the handle went stale.

Source

Thrown at system/src/Grav/Common/Helpers/LogViewer.php:81

        $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));

        fseek($f, -1, SEEK_END);
        if (fread($f, 1) !== "\n") {
            --$lines;
        }

        // Start reading
        $output = '';
        // While we would like more
        while (ftell($f) > 0 && $lines >= 0) {
            // Figure out how far back we should jump
            $seek = min(ftell($f), $buffer);
            // Do the jump (backwards, relative to where we are)
            fseek($f, -$seek, SEEK_CUR);
            // Read a chunk and prepend it to our output
            $chunk = fread($f, $seek);
            if ($chunk === false) {
                throw new \RuntimeException('Cannot read file');
            }
            $output = $chunk . $output;
            // Jump back to where we started reading
            fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
            // Decrease our line counter
            $lines -= substr_count($chunk, "\n");
        }
        // While we have too many lines
        // (Because of buffer size we might have read too many)
        while ($lines++ < 0) {
            // Find first newline and remove all text before that
            $output = substr($output, strpos($output, "\n") + 1);
        }
        // Close file and return
        fclose($f);

        return trim($output);
    }

View on GitHub (pinned to 6040efed04)

Solutions

  1. Catch the RuntimeException in your controller and re-open/retry the read once — rotation windows are transient and usually clear immediately.
  2. Right before reading, verify the file is still readable (is_readable) and its size/inode are sane; skip the read if the file vanished.
  3. In the UI, degrade gracefully: show 'log temporarily unavailable' rather than a 500 when this fires.

Example fix

// before
$content = LogViewer::extractTail($filepath, $lines);

// after
try {
    $content = LogViewer::extractTail($filepath, $lines);
} catch (\RuntimeException $e) {
    $content = ''; // file rotated/unreadable mid-read; retry on next request
}
Defensive patterns

Strategy: retry

Validate before calling

if (!is_readable($filepath) || filesize($filepath) === 0) {
    return ''; // nothing to tail right now
}

Try / catch

try {
    $content = LogViewer::extractTail($filepath, $lines);
} catch (\RuntimeException $e) {
    clearstatcache();
    if (is_readable($filepath)) {
        $content = LogViewer::extractTail($filepath, $lines); // one retry after rotation
    } else {
        $content = '';
    }
}

Prevention

When it happens

Trigger: Calling LogViewer's tail extraction while the log file is rotated, truncated, or deleted under the open handle; reading a log whose permissions changed after fopen; handles on flaky network/container storage.

Common situations: Admin log viewer hit at the exact moment logrotate (or Grav's own log rotation) truncates the file; multiple FPM workers where one rotates while another reads; containerized environments with ephemeral log mounts.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/fb9ee2c32e18fce2. Report an issue: GitHub.