You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Undefined array key 1 in LaravelLog when entry is corrupt/null bytes → HTTP 500 #529
Opening a Laravel daily log file in Log Viewer returns HTTP 500 when the indexed entry is corrupt (e.g. a run of null bytes \x00). The UI fails for the whole file, not just that entry.
Environment
opcodesio/log-viewerv3.24.2
Laravel 13 / PHP 8.5
Shared RWX volume with concurrent writers (daily log under storage/logs/...)
Error
ErrorException: Undefined array key 1 at vendor/opcodesio/log-viewer/src/Logs/LaravelLog.php:41
In our case the constructed text was ~328 null bytes at file offset ~512608.
Root cause
In LaravelLog::parseText():
preg_match(static::regexPattern(), array_shift($firstLineSplit), $matches);
$this->datetime = Carbon::parse($matches[1])?->setTimezone(LogViewer::timezone());
If preg_match fails (corrupt / binary / non-Laravel chunk), $matches[1] is undefined. Laravel turns that into an ErrorException, which bubbles up as a 500 on GET /log-viewer/api/logs.
Also, IndexedLogReader::next() only skips empty($text). A string of \x00 is not empty in PHP, so the corrupt chunk is still passed to the parser.
Expected behaviour
Corrupt or non-matching chunks should not take down the API. Prefer one of:
Treat as an unparseable entry (level none, message like [unparseable log entry]), or
Skip the entry and continue pagination
Valid Laravel lines should keep working unchanged.
Suggested fix
Guard before using$matches[1], e.g.:
if text is empty / only null bytes & whitespace → mark unparseable and return
if preg_match(...) !== 1 → mark unparseable andreturn (do not call Carbon::parse($matches[1]))
Happy to open a PRif that approach works for you.
Workaround
We temporarily register a subclass via LogViewer::extend('laravel', ResilientLaravelLog::class) that catches thiscase locally.
I went with your suggestion of guarding the match, with one change: instead of marking everything unparseable, it falls back to static::$regex first. regexPattern() turns out to be stricter than the regex the indexer already used to accept the entry, so plain lines were reaching this path as well — a millisecond precision timestamp, or anything printed before the [. The fallback keeps their datetime, level and environment; only content matching neither regex is marked unparseable.
Left IndexedLogReader::next() alone, since its empty($text) check returns null and ends iteration rather than skipping, so changing it there would cut log lists short.
Sorry if you were already partway through a patch — happy to close mine if you have one.
Activity
MoheyElbaz commentedon Sep 9, 2026
I hit this too and opened #532.
I went with your suggestion of guarding the match, with one change: instead of marking everything unparseable, it falls back to
static::$regexfirst.regexPattern()turns out to be stricter than the regex the indexer already used to accept the entry, so plain lines were reaching this path as well — a millisecond precision timestamp, or anything printed before the[. The fallback keeps their datetime, level and environment; only content matching neither regex is marked unparseable.Left
IndexedLogReader::next()alone, since itsempty($text)check returns null and ends iteration rather than skipping, so changing it there would cut log lists short.Sorry if you were already partway through a patch — happy to close mine if you have one.