Summary
With AUTH_ROLES_ENABLE=true, any request that reaches Heimdall without the roles header returns HTTP 500. The header is read out of $_SERVER with no guard, inside a view()->composer('*') callback, so it takes down every page rather than the one route that needs the roles.
Root cause
app/Providers/AppServiceProvider.php, in the composer registered for all views:
if (config('app.auth_roles_enable')) {
$view->with('enable_auth_admin_controls', in_array(config('app.auth_roles_admin'), explode(config('app.auth_roles_delimiter'), $_SERVER[config('app.auth_roles_http_header')])));
}
$_SERVER[config('app.auth_roles_http_header')] is unconditional. On PHP 8 a missing key raises ErrorException, and because the composer is registered against * there is no view that can render without it.
ItemController::dash() reads the same header the other way, through the request object, which returns null rather than throwing:
$roles = explode(config('app.auth_roles_delimiter'), $request->header(config('app.auth_roles_header')));
That one does not fatal, but it passes null to explode(), which has been deprecated as a string argument since PHP 8.1.
Observed
Heimdall 2.8.2 (linuxserver/heimdall:2.8.2-ls362):
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
500
storage/logs/laravel-*.log:
[2026-09-08 23:05:32] local.ERROR: Undefined array key "HTTP_X_TOKEN_USER_ROLES" {"exception":"[object] (ErrorException(code: 0): Undefined array key "HTTP_X_TOKEN_USER_ROLES" at /app/www/app/Providers/AppServiceProvider.php:95)
Reproduce
- Set
AUTH_ROLES_ENABLE=true and the other AUTH_ROLES_* variables.
- Request any view route without the roles header, for example on the container's published port rather than through the proxy.
- HTTP 500, with the log line above.
Why it survived
In a working deployment the proxy sets the header on every request that reaches the app, so the key is never missing in the path anyone exercises. It goes missing only where something bypasses the proxy: a container healthcheck, a monitoring probe on the published port, a request during the window before the proxy is configured, or a direct hit on the LAN address. Each of those returns 500 rather than a page, and in the last case that is the whole first impression of a new roles-mode setup.
Proposed fix
Coalesce the missing header to an empty string, which fails closed on the admin check immediately below it: no header means no roles, never the admin role. Do the same for $request->header(...) in dash() so the deprecated explode(string, null) goes away with it.
PR to follow.
Summary
With
AUTH_ROLES_ENABLE=true, any request that reaches Heimdall without the roles header returns HTTP 500. The header is read out of$_SERVERwith no guard, inside aview()->composer('*')callback, so it takes down every page rather than the one route that needs the roles.Root cause
app/Providers/AppServiceProvider.php, in the composer registered for all views:$_SERVER[config('app.auth_roles_http_header')]is unconditional. On PHP 8 a missing key raisesErrorException, and because the composer is registered against*there is no view that can render without it.ItemController::dash()reads the same header the other way, through the request object, which returnsnullrather than throwing:That one does not fatal, but it passes
nulltoexplode(), which has been deprecated as a string argument since PHP 8.1.Observed
Heimdall 2.8.2 (
linuxserver/heimdall:2.8.2-ls362):storage/logs/laravel-*.log:Reproduce
AUTH_ROLES_ENABLE=trueand the otherAUTH_ROLES_*variables.Why it survived
In a working deployment the proxy sets the header on every request that reaches the app, so the key is never missing in the path anyone exercises. It goes missing only where something bypasses the proxy: a container healthcheck, a monitoring probe on the published port, a request during the window before the proxy is configured, or a direct hit on the LAN address. Each of those returns 500 rather than a page, and in the last case that is the whole first impression of a new roles-mode setup.
Proposed fix
Coalesce the missing header to an empty string, which fails closed on the admin check immediately below it: no header means no roles, never the admin role. Do the same for
$request->header(...)indash()so the deprecatedexplode(string, null)goes away with it.PR to follow.