Skip to content

[Bug]:php artisan serve fails on Windows when $_ENV contains mixed-case keys (e.g. Herd PHP with variables_order=EGPCS) #1697

@wangchenxudev

Description

@wangchenxudev

Platform

Windows

Operating system version

windows11 for workstation 25H2

System architecture

Intel (x86)

Herd Version

1.28.0

PHP Version

PHP 8.5.6 (NTS Visual C++ 2022 x64)

Bug description

On Windows, php artisan serve fails when Herd PHP is using variables_order=EGPCS.

Running php artisan serve without --no-reload repeatedly prints:

Failed to listen on 127.0.0.1:8000 (reason: ?)
Failed to listen on 127.0.0.1:8001 (reason: ?)
Failed to listen on 127.0.0.1:8002 (reason: ?)
...

None of those ports are actually in use.

The same project starts correctly with:

  • php artisan serve --no-reload
  • php -S 127.0.0.1:8000 -t public
  • Directly invoking Herd PHP's underlying binary with -S

The issue also disappears if Herd PHP's php.ini is changed from:

variables_order = EGPCS

to:

variables_order = GPCS

Expected behavior:

php artisan serve should start the PHP built-in development server on http://127.0.0.1:8000, the same way php artisan serve --no-reload and php -S 127.0.0.1:8000 -t public do.

Environment:

  • OS: Windows 11 for Workstations x64
  • Herd: 1.28.0
  • PHP: 8.5.6 NTS, Visual C++ 2022 x64, Herd-bundled
  • Laravel: 13.8.0
  • Composer: 2.9.5
  • Loaded php.ini: C:\Users\<USER>\.config\herd\bin\php85\php.ini
  • variables_order: EGPCS

After instrumenting Illuminate\Foundation\Console\ServeCommand::startProcess(), the suspected cause is that Laravel filters the child process environment using a case-sensitive whitelist.

On Windows, environment variables are case-insensitive at the OS level, but PHP array keys are case-sensitive. With variables_order=EGPCS, $_ENV contains keys using their original Windows casing, such as Path, SystemRoot, windir, and ComSpec.

However, ServeCommand::$passthroughVariables contains uppercase entries such as PATH and SYSTEMROOT. Because in_array() is case-sensitive, Path and SystemRoot do not match the whitelist and are mapped to false. Symfony Process then removes them from the child PHP server environment instead of letting them be inherited.

In the failing run, only APP_ENV survived the filter. Windows-critical environment variables such as Path, SystemRoot, windir, ComSpec, TEMP, TMP, USERPROFILE, APPDATA, and LOCALAPPDATA were removed.

This appears to prevent the child PHP built-in server from initializing/binding the socket correctly, which results in the Failed to listen ... (reason: ?) output.

Possible direction, if this analysis is correct:

  • Compare passthrough environment variable names case-insensitively on Windows.
  • Add Windows-critical environment variables to the passthrough list, such as WINDIR, COMSPEC, TEMP, TMP, USERPROFILE, APPDATA, LOCALAPPDATA, ALLUSERSPROFILE, and SYSTEMDRIVE.

I hope this helps narrow down the issue. I’m not very familiar with Laravel’s internal implementation, so I may be missing some context, but I’m happy to provide any additional debugging information if needed.

Steps to reproduce

  1. Install Laravel Herd 1.28.0 on Windows 11.

  2. Confirm Herd PHP 8.5 is the active CLI PHP:

    where.exe php
    php -v
    php --ini

  3. Confirm Herd PHP's loaded php.ini uses:

    variables_order = EGPCS

  4. Create or open a Laravel 13 project.

  5. Confirm the target ports are not occupied, for example:

    Get-NetTCPConnection -LocalAddress 127.0.0.1 -LocalPort 8000,8001,8002,8003 -ErrorAction SilentlyContinue

  6. Run:

    php artisan serve

  7. Observe that it repeatedly fails with:

    Failed to listen on 127.0.0.1:8000 (reason: ?)

  8. Run the following control command:

    php artisan serve --no-reload

    This works.

  9. Run the native PHP development server directly:

    php -S 127.0.0.1:8000 -t public

    This also works.

  10. Change Herd PHP's php.ini from:

    variables_order = EGPCS

    to:

    variables_order = GPCS

  11. Run again:

    php artisan serve

    This now works.

Relevant log output

php artisan serve

Failed to listen on 127.0.0.1:8000 (reason: ?)
Failed to listen on 127.0.0.1:8001 (reason: ?)
Failed to listen on 127.0.0.1:8002 (reason: ?)
Failed to listen on 127.0.0.1:8003 (reason: ?)
Failed to listen on 127.0.0.1:8004 (reason: ?)


php artisan serve --no-reload

INFO  Server running on [http://127.0.0.1:8000].


php artisan about

Environment:
Application Name: Laravel
Laravel Version: 13.8.0
PHP Version: 8.5.6
Composer Version: 2.9.5
Environment: local
Debug Mode: ENABLED
URL: localhost:8000


php --ini

Loaded Configuration File:
C:\Users\<USER>\.config\herd\bin\php85\php.ini


Instrumented ServeCommand::startProcess() findings:

variables_order_ini: EGPCS
option_no_reload: false
env_count: 111

Actual Windows environment keys present in $_ENV:

has_PATH_exact: false
has_Path_exact: true
has_SYSTEMROOT_exact: false
has_SystemRoot_exact: true
has_windir_exact: true
has_WINDIR_exact: false
has_ComSpec_exact: true
has_COMSPEC_exact: false
has_TEMP_exact: true

Per-key filter decisions:

Path:
matches whitelist case-sensitive: false
matches whitelist case-insensitive: true
final decision: REMOVE, set to false

SystemRoot:
matches whitelist case-sensitive: false
matches whitelist case-insensitive: true
final decision: REMOVE, set to false

windir:
matches whitelist case-sensitive: false
matches whitelist case-insensitive: false
final decision: REMOVE, set to false

ComSpec:
matches whitelist case-sensitive: false
matches whitelist case-insensitive: false
final decision: REMOVE, set to false

TEMP:
matches whitelist case-sensitive: false
matches whitelist case-insensitive: false
final decision: REMOVE, set to false

TMP:
matches whitelist case-sensitive: false
matches whitelist case-insensitive: false
final decision: REMOVE, set to false

USERPROFILE:
matches whitelist case-sensitive: false
matches whitelist case-insensitive: false
final decision: REMOVE, set to false

APPDATA:
matches whitelist case-sensitive: false
matches whitelist case-insensitive: false
final decision: REMOVE, set to false

LOCALAPPDATA:
matches whitelist case-sensitive: false
matches whitelist case-insensitive: false
final decision: REMOVE, set to false

Final child process environment in the failing run:

total: 112
kept_count: 1
removed_count: 111
kept_keys: APP_ENV
PATH_removed: true
SystemRoot_removed: true
windir_removed: true

In --no-reload mode, the filter is bypassed and the same project starts correctly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions