[Help] `$_ENV` gets overridden
What happened?
Hello
I'm not sure if this is a bug or just me being stupid, but I can't for the life of me figure out how to populate my $_ENV array inside the worker handler.
Code looks like this. I'm using the Slim framework.
<?php
use Slim\App;
ignore_user_abort(true);
// This loads dotenv, defines $app and populates my $_ENV
require 'app/bootstrap.php';
/** @var App $app */
// $_ENV contains all my expected variables at this point if I dump them, so I copy them into another array:
$env = array_merge($_ENV);
// And use that array in the handler:
$handler = static function () use ($app, $env) {
$_ENV = array_merge($_ENV, $env);
// At this point, $_ENV still contains what I expect it to if I error_log(json_encode($_ENV))
// But inside my $app, all the $_ENV variables I can normally access no longer contain my values;
// they get overridden with what looks like caddy environment variables, even though the $_ENV array
// is correct just above this call?
// In my $app, I simply use the array like $myValue = $_ENV['MY_VAR_KEY'], so no funny-business.
$app->run();
};
while (true) {
$keepRunning = \frankenphp_handle_request($handler);
gc_collect_cycles();
if (!$keepRunning) {
break;
}
}
I cannot understand how this would happen, ~~and after a few requests, I get blank 200 OK but no error and no logs. I suspect this is a different issue, so I'm trying to figure out the env thing first.~~ Unrelated, fixed.
Build Type
Docker (Debian Bookworm)
Worker Mode
Yes
Operating System
macOS
CPU Architecture
Apple Silicon
PHP configuration
It's just the default frankenphp docker image (:latest as of this issue) with the following php.ini loaded:
session.use_strict_mode = 1
session.use_cookies = 1
session.cookie_httponly = 1
session.sid_length = 54
session.sid_bits_per_character = 5
session.gc_probability = 0
zend_extension = opcache.so
opcache.enable = 1
opcache.jit = disable
opcache.jit_buffer_size = 128M
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
opcache.enable_cli=1
upload_max_filesize = 10M
post_max_size = 10M
Relevant log output
I just get: Undefined array key "MY_VAR_KEY" from a class inside $app.
Isn't Slim using a DotEnv parsing library or something like that overriding $_ENV? It's something quite common in frameworks.
Isn't Slim using a DotEnv parsing library or something like that overriding
$_ENV? It's something quite common in frameworks.
No. I load dotenv myself and set the variables that way. If what you suggested here was the issue, it wouldn't work without frankenphp either, but it does.
Edit: I guess my question is: What is the recommended way to access environment variables inside the closure?
The most bizarre thing here is that if I hit the worker script multiple times, it will populate the environment keys one-by-one and error each time it reaches an undefined one. Eventually it will work until I switch to an endpoint that requires a new key. Really, quite confusing.
You'd think that with this kind of error, the first request would populate all the keys and then potentially fail, and then it would work on the second request and onwards for every endpoint. I'm not even sure where to begin with this problem.
Edit: I'm using worker count 1 to keep the variables to a minimum, and I have confirmed that the bootstrap logic before the handler is only run once.
I'm also seeing some weird behavior where different threads seem to have different instances of $_ENV in worker mode.
@nickdnk are you seeing the same issue when using $_SERVER insted of $_ENV?
I'm also seeing some weird behavior where different threads seem to have different instances of $_ENV in worker mode. @nickdnk are you seeing the same issue when using $_SERVER insted of $_ENV?
I'm not sure, I'll have to check. But using $_SERVER is kind of dangerous for this, as any key starting with HTTP_ will be overriden by the headers of the incoming HTTP request. Mixing your own, potentially sensitive, parameters in there with user data is probably not a great idea.
It looks to me like the most robust method would be to just create your own PHP class that reads the initial $_ENV after the bootstrap and just use that class in your app whenever you need an env, like EnvHandler::get('API_TOKEN_EXAMPLE') instead of $_ENV['API_TOKEN_EXAMPLE]. This is also fairly easy to stick with even if $_ENV works correctly, as you'll just call return $_ENV['API_TOKEN_EXAMPLE] ?? null inside the function then.
@AlliBalliBaba Are you sure that none of you codebase is using putenv() or getenv(), that are known to not be thread safe?
Anyway, a minimal reproducer would help understanding what's going on.
I was able to reproduce a similar behaviour with putenv, variables_order = "EGPCS" and the following worker script. Only some threads will have APP_ENV=local in $_ENV. I only noticed it since Laravel still calls putenv somewhere when not caching configurations.
putenv('APP_ENV=local');
$print = function () {
echo "<pre>";
print_r($_ENV);
echo "</pre>";
};
while (frankenphp_handle_request($print)) {
}
I would generally advice against using putenv though, so not sure if this needs fixing.
As a side note, It's perfectly fine to use $_SERVER. Using $_SERVER is only unsafe if you prefix your environment variables with HTTP_ or echo out all variables as plain html.
@AlliBalliBaba Are you sure that none of you codebase is using
putenv()orgetenv(), that are known to not be thread safe?Anyway, a minimal reproducer would help understanding what's going on.
I've managed to reproduce it here: https://github.com/nickdnk/frankenphp-var-demo
It seems it has to do with some sort of static vs. non-static logic. There is a static class which isn't part of the bootstrapping process at all (this seemed to be crucial; that it's not loaded in index.php) that gets called inside the handler, and for some reason, something goes wrong with reading $_ENV in there.
It makes no sense to me at all, but if you just docker compose up -d and curl https://localhost --insecure, it will error on the first run and work fine on all subsequent runs. You can watch the docker logs to observe that the $_ENV value does load correctly before the handler is called by frankenphp.
Edit: This was really hard to debug, because it works fine if you don't use the autoloader and just require the classes directly in the bootstrap process. I've spent probably 3 hours figuring out how to reproduce it outside my own app. I hope you can use it.
Edit 2: Okay it doesn't seem to matter if the second class is static or not, as long as it's not loaded in index.php. I just tested with (new SomeOtherClass())->someFunction(); inside the handler instead, and the behavior is identical. I just went with static because that's how it was used in my app.
@AlliBalliBaba Sure, you can use $_SERVER, I would just prefer to use $_ENV as it doesn't have any user input ever.
side note: $_ENV is nothing but user input :) like $_GET or $_COOKIE, it's just that the people doing the inputting are highly skilled programmers/hackers. You shouldn't trust it blindly.
This smells suspiciously like a PHP bug... We don't modify the environment once the worker is loaded (at least, we shouldn't be).
side note:
$_ENVis nothing but user input :) like$_GETor$_COOKIE, it's just that the people doing the inputting are highly skilled programmers/hackers. You shouldn't trust it blindly.This smells suspiciously like a PHP bug... We don't modify the environment once the worker is loaded (at least, we shouldn't be).
Sure, but again, my point is that under normal circumstances, you won't see arbitrary user input (from your incoming HTTP requests) in your $_ENV array, unless you're doing something you (probably) shouldn't. You will in $_SERVER.
I think it may be a PHP bug as well, yes, I just don't observe this behavior when I don't wrap my application in the frankenphp handler logic.
Thanks for the reproducer. Not sure if it's a PHP bug, we manipulate superglobals in worker mode and it may also be a bug here: https://github.com/dunglas/frankenphp/blob/main/frankenphp.c#L156
Or even here: https://github.com/dunglas/frankenphp/blob/main/frankenphp.c#L156
Maybe should we call this function for $_ENV too: https://github.com/dunglas/frankenphp/blob/main/frankenphp.c#L195
Ah, I see. You are modifying $_ENV directly from PHP and trying to read the value. This does seem like a strange php-level bug.
FWIW, using $_ENV to read the environment ($_ENV['CADDY_GLOBAL_OPTIONS']) works as it should. That being said, this is a weird one.
To summarize:
- Set
$_ENV['var']to a value - Later, read
$_ENV['var'], and won't be set
I will step through the c-side and see what is going on, but after further testing, I suspect it is working as designed.
I was able to reproduce a similar behaviour with putenv, variables_order = "EGPCS" and the following worker script. Only some threads will have APP_ENV=local in $_ENV. I only noticed it since Laravel still calls putenv somewhere when not caching configurations.
IIRC, $_ENV is a snapshot of the env on the first request of it (or at the beginning of execution). It doesn't change or update during execution. If you use putenv to actually change the environment, any future calls to $_ENV won't be set with it unless it is the first read of $_ENV.
This is outside my wheel house, the really odd thing is that setting the variable works, but only for the PHP files already loaded when you set it. Remember that it prints out the expected value right before the call to $app->run, but within $app, you get the unmodified version.
Yeah, what is really weird is it is like you said, it depends on the file. It's almost like it is getting compiled weird. Stepping through compilation is a PITA to see what is going on ... but I'll update here once I finish my F7/F8 marathon.
Yeah, so this is a very weird PHP/TSRM bug. The ZEND_VM handler for require/include "loses" the actual $_ENV variable somehow (not sure which one it points to yet). So on the first execution, during the autoloading and just after the autoloading, you are accessing the wrong $_ENV. Once the autoloading has completed, you access the correct $_ENV.
The VM handler for include/require is the same as eval so this is likely some security thing being executed accidentally, but I'm not sure how this is getting reset.
I'm working on reproducing in a test case for reporting the issue to php-src. If I happen to find the fix along the way, then I will open a PR over there as well.
ok sorry for the noise, just trying out runtime+worker in my mature Symfony app, and falling foul of env issues too.
My app works (even in prod) with php-fpm/nginx and with just Caddy and Frankenphp, but when adding your runtime and worker the same app (no other changes) - errors.
On first page load get an error that HTTP_PROXY Environment variable not found - this var is defined in my .env for the project (as I proxy outgoing http)
On the second page load I get into my app, but then the twig environment global app is not available. (as in app.user.id)
running the exact same code base, with Caddy/Frankenphp without the runtime/worker, the app and env vars work perfectly.
Does this sound like the same bug?
Fatal error: Uncaught Symfony\Component\DependencyInjection\Exception\EnvNotFoundException: Environment variable not found: "HTTP_PROXY". in /app/vendor/symfony/dependency-injection/EnvVarProcessor.php:221 Stack trace: #0 /app/vendor/symfony/dependency-injection/Container.php(370): Symfony\Component\DependencyInjection\EnvVarProcessor->getEnv('string', 'HTTP_PROXY', Object(Closure)) #1 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(1892): Symfony\Component\DependencyInjection\Container->getEnv('HTTP_PROXY') #2 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(1625): ContainerNri1Iv4\App_KernelDevDebugContainer::getTraceableHttpClientService(Object(ContainerNri1Iv4\App_KernelDevDebugContainer)) #3 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(834): ContainerNri1Iv4\App_KernelDevDebugContainer::get_Debug_HttpClientService(Object(ContainerNri1Iv4\App_KernelDevDebugContainer)) #4 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(2032): ContainerNri1Iv4\App_KernelDevDebugContainer::get_Container_Private_ProfilerService(Object(ContainerNri1Iv4\App_KernelDevDebugContainer)) #5 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(1293): ContainerNri1Iv4\App_KernelDevDebugContainer::getAssetMapper_DevServerSubscriberService(Object(ContainerNri1Iv4\App_KernelDevDebugContainer)) #6 /app/vendor/symfony/event-dispatcher/EventDispatcher.php(221): ContainerNri1Iv4\App_KernelDevDebugContainer::ContainerNri1Iv4\{closure}() #7 /app/vendor/symfony/event-dispatcher/EventDispatcher.php(70): Symfony\Component\EventDispatcher\EventDispatcher->sortListeners('kernel.request') #8 /app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php(252): Symfony\Component\EventDispatcher\EventDispatcher->getListeners('kernel.request') #9 /app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php(116): Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->preProcess('kernel.request') #10 /app/vendor/symfony/http-kernel/HttpKernel.php(159): Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch(Object(Symfony\Component\HttpKernel\Event\RequestEvent), 'kernel.request') #11 /app/vendor/symfony/http-kernel/HttpKernel.php(76): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 1) #12 /app/vendor/symfony/http-kernel/Kernel.php(182): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #13 /app/vendor/runtime/frankenphp-symfony/src/Runner.php(38): Symfony\Component\HttpKernel\Kernel->handle(Object(Symfony\Component\HttpFoundation\Request)) #14 [internal function]: Runtime\FrankenPhpSymfony\Runner->Runtime\FrankenPhpSymfony\{closure}() #15 /app/vendor/runtime/frankenphp-symfony/src/Runner.php(45): frankenphp_handle_request(Object(Closure)) #16 /app/vendor/autoload_runtime.php(29): Runtime\FrankenPhpSymfony\Runner->run() #17 /app/public/index.php(12): require_once('/app/vendor/aut...') #18 {main} Next Symfony\Component\DependencyInjection\Exception\EnvNotFoundException: Environment variable not found: "HTTP_PROXY". in /app/vendor/symfony/dependency-injection/EnvVarProcessor.php:221 Stack trace: #0 /app/vendor/symfony/dependency-injection/Container.php(370): Symfony\Component\DependencyInjection\EnvVarProcessor->getEnv('string', 'HTTP_PROXY', Object(Closure)) #1 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(1892): Symfony\Component\DependencyInjection\Container->getEnv('HTTP_PROXY') #2 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(1625): ContainerNri1Iv4\App_KernelDevDebugContainer::getTraceableHttpClientService(Object(ContainerNri1Iv4\App_KernelDevDebugContainer)) #3 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(1934): ContainerNri1Iv4\App_KernelDevDebugContainer::get_Debug_HttpClientService(Object(ContainerNri1Iv4\App_KernelDevDebugContainer)) #4 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(2491): ContainerNri1Iv4\App_KernelDevDebugContainer::getTraceableHttpClient2Service(Object(ContainerNri1Iv4\App_KernelDevDebugContainer)) #5 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(1954): ContainerNri1Iv4\App_KernelDevDebugContainer::getMercure_Hub_Default_TraceableService(Object(ContainerNri1Iv4\App_KernelDevDebugContainer)) #6 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(3280): ContainerNri1Iv4\App_KernelDevDebugContainer::getHubRegistryService(Object(ContainerNri1Iv4\App_KernelDevDebugContainer)) #7 /app/var/build/ContainerNri1Iv4/getErrorControllerService.php(29): ContainerNri1Iv4\App_KernelDevDebugContainer::getTwigService(Object(ContainerNri1Iv4\App_KernelDevDebugContainer)) #8 /app/var/build/ContainerNri1Iv4/App_KernelDevDebugContainer.php(804): ContainerNri1Iv4\getErrorControllerService::do(Object(ContainerNri1Iv4\App_KernelDevDebugContainer), true) #9 /app/vendor/symfony/dependency-injection/Container.php(221): ContainerNri1Iv4\App_KernelDevDebugContainer->load('getErrorControl...') #10 /app/vendor/symfony/dependency-injection/Container.php(203): Symfony\Component\DependencyInjection\Container::make(Object(ContainerNri1Iv4\App_KernelDevDebugContainer), 'error_controlle...', 1) #11 /app/vendor/symfony/http-kernel/Controller/ContainerControllerResolver.php(38): Symfony\Component\DependencyInjection\Container->get('error_controlle...') #12 /app/vendor/symfony/framework-bundle/Controller/ControllerResolver.php(25): Symfony\Component\HttpKernel\Controller\ContainerControllerResolver->instantiateController('error_controlle...') #13 /app/vendor/symfony/http-kernel/Controller/ControllerResolver.php(115): Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver->instantiateController('error_controlle...') #14 /app/vendor/symfony/http-kernel/Controller/ControllerResolver.php(95): Symfony\Component\HttpKernel\Controller\ControllerResolver->createController('error_controlle...') #15 /app/vendor/symfony/http-kernel/Controller/TraceableControllerResolver.php(33): Symfony\Component\HttpKernel\Controller\ControllerResolver->getController(Object(Symfony\Component\HttpFoundation\Request)) #16 /app/vendor/symfony/http-kernel/HttpKernel.php(166): Symfony\Component\HttpKernel\Controller\TraceableControllerResolver->getController(Object(Symfony\Component\HttpFoundation\Request)) #17 /app/vendor/symfony/http-kernel/HttpKernel.php(76): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 2) #18 /app/vendor/symfony/http-kernel/EventListener/ErrorListener.php(97): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 2, false) #19 /app/vendor/symfony/event-dispatcher/Debug/WrappedListener.php(115): Symfony\Component\HttpKernel\EventListener\ErrorListener->onKernelException(Object(Symfony\Component\HttpKernel\Event\ExceptionEvent), 'kernel.exceptio...', Object(Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher)) #20 /app/vendor/symfony/event-dispatcher/EventDispatcher.php(206): Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke(Object(Symfony\Component\HttpKernel\Event\ExceptionEvent), 'kernel.exceptio...', Object(Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher)) #21 /app/vendor/symfony/event-dispatcher/EventDispatcher.php(56): Symfony\Component\EventDispatcher\EventDispatcher->callListeners(Array, 'kernel.exceptio...', Object(Symfony\Component\HttpKernel\Event\ExceptionEvent)) #22 /app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php(122): Symfony\Component\EventDispatcher\EventDispatcher->dispatch(Object(Symfony\Component\HttpKernel\Event\ExceptionEvent), 'kernel.exceptio...') #23 /app/vendor/symfony/http-kernel/HttpKernel.php(241): Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch(Object(Symfony\Component\HttpKernel\Event\ExceptionEvent), 'kernel.exceptio...') #24 /app/vendor/symfony/http-kernel/HttpKernel.php(91): Symfony\Component\HttpKernel\HttpKernel->handleThrowable(Object(Symfony\Component\DependencyInjection\Exception\EnvNotFoundException), Object(Symfony\Component\HttpFoundation\Request), 1) #25 /app/vendor/symfony/http-kernel/Kernel.php(182): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #26 /app/vendor/runtime/frankenphp-symfony/src/Runner.php(38): Symfony\Component\HttpKernel\Kernel->handle(Object(Symfony\Component\HttpFoundation\Request)) #27 [internal function]: Runtime\FrankenPhpSymfony\Runner->Runtime\FrankenPhpSymfony\{closure}() #28 /app/vendor/runtime/frankenphp-symfony/src/Runner.php(45): frankenphp_handle_request(Object(Closure)) #29 /app/vendor/autoload_runtime.php(29): Runtime\FrankenPhpSymfony\Runner->run() #30 /app/public/index.php(12): require_once('/app/vendor/aut...') #31 {main} Next InvalidArgumentException: The controller for URI "/en/sites/" is not callable: Environment variable not found: "HTTP_PROXY". in /app/vendor/symfony/http-kernel/Controller/ControllerResolver.php:97 Stack trace: #0 /app/vendor/symfony/http-kernel/Controller/TraceableControllerResolver.php(33): Symfony\Component\HttpKernel\Controller\ControllerResolver->getController(Object(Symfony\Component\HttpFoundation\Request)) #1 /app/vendor/symfony/http-kernel/HttpKernel.php(166): Symfony\Component\HttpKernel\Controller\TraceableControllerResolver->getController(Object(Symfony\Component\HttpFoundation\Request)) #2 /app/vendor/symfony/http-kernel/HttpKernel.php(76): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 2) #3 /app/vendor/symfony/http-kernel/EventListener/ErrorListener.php(97): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 2, false) #4 /app/vendor/symfony/event-dispatcher/Debug/WrappedListener.php(115): Symfony\Component\HttpKernel\EventListener\ErrorListener->onKernelException(Object(Symfony\Component\HttpKernel\Event\ExceptionEvent), 'kernel.exceptio...', Object(Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher)) #5 /app/vendor/symfony/event-dispatcher/EventDispatcher.php(206): Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke(Object(Symfony\Component\HttpKernel\Event\ExceptionEvent), 'kernel.exceptio...', Object(Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher)) #6 /app/vendor/symfony/event-dispatcher/EventDispatcher.php(56): Symfony\Component\EventDispatcher\EventDispatcher->callListeners(Array, 'kernel.exceptio...', Object(Symfony\Component\HttpKernel\Event\ExceptionEvent)) #7 /app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php(122): Symfony\Component\EventDispatcher\EventDispatcher->dispatch(Object(Symfony\Component\HttpKernel\Event\ExceptionEvent), 'kernel.exceptio...') #8 /app/vendor/symfony/http-kernel/HttpKernel.php(241): Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch(Object(Symfony\Component\HttpKernel\Event\ExceptionEvent), 'kernel.exceptio...') #9 /app/vendor/symfony/http-kernel/HttpKernel.php(91): Symfony\Component\HttpKernel\HttpKernel->handleThrowable(Object(Symfony\Component\DependencyInjection\Exception\EnvNotFoundException), Object(Symfony\Component\HttpFoundation\Request), 1) #10 /app/vendor/symfony/http-kernel/Kernel.php(182): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #11 /app/vendor/runtime/frankenphp-symfony/src/Runner.php(38): Symfony\Component\HttpKernel\Kernel->handle(Object(Symfony\Component\HttpFoundation\Request)) #12 [internal function]: Runtime\FrankenPhpSymfony\Runner->Runtime\FrankenPhpSymfony\{closure}() #13 /app/vendor/runtime/frankenphp-symfony/src/Runner.php(45): frankenphp_handle_request(Object(Closure)) #14 /app/vendor/autoload_runtime.php(29): Runtime\FrankenPhpSymfony\Runner->run() #15 /app/public/index.php(12): require_once('/app/vendor/aut...') #16 {main} thrown in /app/vendor/symfony/http-kernel/Controller/ControllerResolver.php on line 97
@PhilETaylor it might be a similar issue. Are you using variables_order = EGPCS in your php.ini? Are you seeing the same issues when using variables_order = GPCS?
my php.ini is barely touched from standard provided and so I was using EGPCS, I updated my php.ini to GPCS and restarted (after manually removing the symfony cache just in case) and the same issue persisted. Sorry.
I have added #1395 which has full details and a reproducer.