vlucas/phpdotenv · error · Error

Lexer encountered unexpected character [%s].

Error message

Lexer encountered unexpected character [%s].

What it means

Lexer::lex() (src/Parser/Lexer.php:50) tokenizes entry VALUES (EntryParser::parseValue feeds it each value) by matching one of eight anchored patterns covering newlines, whitespace, backslash, quotes, '#', '$', parens, and runs of any other byte; when preg_match fails at an offset it throws PHP \Error — not an Exception — naming the offending character. Two consequences: it is not caught by 'catch (Exception $e)' and it bypasses the Result pipeline that normally produces InvalidFileException [0]. In this exact pattern set every byte is covered, so in practice the throw fires only when the PCRE engine itself fails (preg_match returning false, e.g. limit exhaustion on pathological input); in earlier/modified 5.x pattern sets it surfaced for characters outside the token set.

Source

Thrown at src/Parser/Lexer.php:50

     * handling, for performance reasons.
     *
     * @param string $content
     *
     * @return \Generator<string>
     */
    public static function lex(string $content)
    {
        static $regex;

        if ($regex === null) {
            $regex = '(('.\implode(')|(', self::PATTERNS).'))A';
        }

        $offset = 0;

        while (isset($content[$offset])) {
            if (!\preg_match($regex, $content, $matches, 0, $offset)) {
                throw new \Error(\sprintf('Lexer encountered unexpected character [%s].', $content[$offset]));
            }

            $offset += \strlen($matches[0]);

            yield $matches[0];
        }
    }
}

View on GitHub (pinned to 416df70283)

Solutions

  1. Catch \Error (or \Throwable) around load() — 'catch (Exception)' will miss it.
  2. Search the .env for the character named between [ and ] and remove/quote it.
  3. Re-save the .env from a plain-text editor as UTF-8 to strip control/binary bytes.
  4. composer update vlucas/phpdotenv within your major version — the current PATTERNS set covers all bytes.
  5. If you patch Lexer::PATTERNS, verify every byte 0x00-0xFF still matches at least one alternative.

Example fix

// before
try {
    $dotenv->load();
} catch (\Exception $e) {       \Error escapes this
    log($e->getMessage());
}

// after
try {
    $dotenv->load();
} catch (\Throwable $e) {       \ catches both \Error and the Dotenv exceptions
    log($e->getMessage());
}
Defensive patterns

Strategy: try-catch

Try / catch

// The lexer throws PHP \Error, not an Exception — catch \Throwable or \Error:
try {
    Dotenv::createImmutable($dir)->load();
} catch (\Dotenv\Exception\InvalidFileException $e) {
    // normal syntax problems, see error 0
} catch (\Error $e) {
    if (str_contains($e->getMessage(), 'Lexer encountered unexpected character')) {
        // isolate the named character, normalize/re-save the .env as UTF-8, then retry once
    }
    throw $e;
}

Prevention

When it happens

Trigger: Any ->load()/safeLoad()/Dotenv::parse() call whose VALUE region makes the anchored preg_match fail: PCRE errors (backtrack/recursion limits hit on adversarial or degenerate content); content that breaks a customized Lexer::PATTERNS in a forked/patched vendor copy; older 5.x releases where stray characters (control bytes, unusual punctuation) matched no token. Note plain syntax mistakes (spaces, bad escapes, unclosed quotes) normally surface as InvalidFileException [0] instead — the lexer tokenizes them fine and EntryParser rejects them.

Common situations: Fuzzed or machine-generated .env content; vendor tree patched by a Composer patch changing the lexer; extremely long single-line values in older versions; upgrading across 5.x point releases where PATTERNS changed.

Related errors


AI-assisted analysis of vlucas/phpdotenv@416df70283 (2026-08-21). Data as JSON: /api/errors/36c5e7bd8995d9e0. Report an issue: GitHub.