Skip to content

Undefined array key 1 in LaravelLog when entry is corrupt/null bytes → HTTP 500 #529

Description

@xaviergainon

Summary

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-viewer v3.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

Relevant stack (abridged):
Opcodes\LogViewer\Logs\LaravelLog->parseText() Opcodes\LogViewer\Logs\Log->__construct('\x00\x00\x00...', ..., $filePosition, $index) Opcodes\LogViewer\Readers\IndexedLogReader->next() Opcodes\LogViewer\Http\Controllers\LogsController->index()

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 and return (do not call Carbon::parse($matches[1]))
Happy to open a PR if that approach works for you.

Workaround
We temporarily register a subclass via LogViewer::extend('laravel', ResilientLaravelLog::class) that catches this case locally.

Activity

  1. MoheyElbaz commented on Sep 9, 2026

    @MoheyElbaz

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions