Problem/Motivation
3rd party contrib streamWrappers may(and do) call the Drupal Container (\Drupal::service()) during __construct() to obtain necessary services for their operation (maybe we shouldn't, however its been the case since at least D8.9, I wouldn't expect this to change in a minor).
After #3453216: Clean up outdated mentions of prepareLegacyRequest the Drupal Kernel attempts to register streamWrappers before the container has been fully populated with causes a ServiceNotFoundException.
Seen in #3601847: D11.4, s3fs service not found.
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: You have requested a non-existent service "s3fs". in Drupal\Component\DependencyInjection\Container->get() (line 153 of core/lib/Drupal/Component/DependencyInjection/Container.php).
Drupal::service() (Line: 181)
Drupal\s3fs\StreamWrapper\S3fsStream->__construct() (Line: 262)
Drupal\Component\DependencyInjection\Container->createService() (Line: 173)
Drupal\Component\DependencyInjection\Container->get() (Line: 453)
Drupal\Component\DependencyInjection\Container->{closure:Drupal\Component\DependencyInjection\Container::resolveServicesAndParameters():452}() (Line: 61)
Symfony\Component\DependencyInjection\ServiceLocator->doGet() (Line: 43)
Symfony\Component\DependencyInjection\ServiceLocator->get() (Line: 110)
Drupal\Core\StreamWrapper\StreamWrapperManager->getClass() (Line: 143)
Drupal\Core\StreamWrapper\StreamWrapperManager->register() (Line: 1073)
Drupal\Core\DrupalKernel->initializeContainer() (Line: 881)
Drupal\Core\DrupalKernel->updateModules() (Line: 722)
Drupal\Core\Extension\ModuleInstaller->updateKernel() (Line: 320)
Drupal\Core\Extension\ModuleInstaller->doInstall() (Line: 229)
Drupal\Core\Extension\ModuleInstaller->install() (Line: 503)
Drupal\system\Form\ModulesListForm->submitForm() (Line: 108)
Drupal\Core\Form\FormSubmitter->executeSubmitHandlers() (Line: 45)
Drupal\Core\Form\FormSubmitter->doSubmitForm() (Line: 620)
Drupal\Core\Form\FormBuilder->processForm() (Line: 343)
Drupal\Core\Form\FormBuilder->buildForm() (Line: 73)
Drupal\Core\Controller\FormController->getContentResult()
call_user_func_array() (Line: 123)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->{closure:Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber::wrapControllerExecutionInRenderContext():121}() (Line: 638)
Drupal\Core\Render\Renderer::{closure:Drupal\Core\Render\Renderer::executeInRenderContext():638}()
Fiber->resume() (Line: 653)
Drupal\Core\Render\Renderer->executeInRenderContext() (Line: 121)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext() (Line: 97)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->{closure:Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber::onController():96}() (Line: 183)
Symfony\Component\HttpKernel\HttpKernel->handleRaw() (Line: 76)
Symfony\Component\HttpKernel\HttpKernel->handle() (Line: 53)
Drupal\Core\StackMiddleware\Session->handle() (Line: 30)
Drupal\Core\StackMiddleware\KernelPreHandle->handle() (Line: 28)
Drupal\Core\StackMiddleware\ContentLength->handle() (Line: 32)
Drupal\big_pipe\StackMiddleware\ContentLength->handle() (Line: 118)
Drupal\page_cache\StackMiddleware\PageCache->pass() (Line: 92)
Drupal\page_cache\StackMiddleware\PageCache->handle() (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle() (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle() (Line: 61)
Drupal\Core\StackMiddleware\AjaxPageState->handle() (Line: 54)
Drupal\Core\StackMiddleware\StackedHttpKernel->handle() (Line: 753)
Drupal\Core\DrupalKernel->handle() (Line: 35)
Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner->run() (Line: 32)
require('/var/www/html/vendor/autoload_runtime.php') (Line: 22)
require_once('/var/www/html/web/autoload_runtime.php') (Line: 13)Steps to reproduce
Install a module with a streamWrapper that makes a \Drupal::service('module.service') call to a service located only in the module providing the streamWrapper.
Proposed resolution
Ensure service container is populated before registering streamWrappers.
Remaining tasks
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
Issue fork drupal-3603333
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 3603333-stream-wrapper-registration
changes, plain diff MR !16301
Comments
Comment #2
cmlaraA few other streamWrappers that appear to have same fault now:
https://www.drupal.org/project/media_avportal
https://www.drupal.org/project/gcsfs
https://www.drupal.org/project/rokka
These were all a simple "download module, install module", if it failed at that point with a similar stack it was considered to have the fault. No back validation performed on lower core versions.
Note: That depending how the installs fail they can leave the site in an unstable condition. The rokka module can't be uninstalled via the UI(isn't even accessible) or
drush pmudue to multiple database table never created, even thedrush cex; drush cimtrick (where you remove the module from the core.extensions.yml) fails.S3FS took down the entire test lab due to a thrown exception from a dependency library (missing default config, which in fairness we likely need to do something there to help mitigate) with no apparent way to recover the site.
Comment #3
freelockI ran into several issues like this in the 11.3 upgrade cycle, mostly in hook classes - dependency injection led to circular dependency issues. The solution there was to make the classes lazy load their dependencies instead of injecting them. Is that an option here?
Comment #4
cmlaraStreamWrappers don’t support injection (it’s not on the API spec in PHP itself) you have to call the global container.
Technically contrib could move the calls from the constructor to inline to try and delay the request to slightly latter however that create at least a couple concerns:
Point one might look successful at first, howver the moment you add any code that triggers a read before the container is booted will cause abnormal issues or WSOD’s.
Point 2 is more about the fact streamWrapper classes may be API (depends on each contrib module, over in s3fs we do know our streamWrapper has been extended by others) and changing that is a major break, moving the error handling and failure points deeper into code that’s needs to be ready to respond at any time.
We have fought that a few times in S3FS and are starting to learn it’s better to detangle the circle. Anything we did for that in being lazy or avoiding current known usages of circular dependency in the past eventually came back to bite us.
Comment #5
catchIt looks to me like #3583911: Clean up StreamWrapperManager::register() calls might fix this - could you test that MR against s3fs? If it does we'd need to figure out how backportable it is.
If it doesn't help or can't be backported, then we might need to revert #3453216: Clean up outdated mentions of prepareLegacyRequest in 11.x, but would be good to try to go forwrards rather than backwards if we can.
Comment #6
cmlaraTechnically speaking it now fails sooner due to a circular dependency error due to the first line of our constructor being:
For sake of testing I did comment out the circular call and the registration did proceed further, so in concept yes that patch helps however carries its own problems.
I see az_blob_fs and gcsfs appear they would be broken by that patch for same circular dependency flaw.
https://search.tresbien.tech/search?q=streamWrapperManager%20f%3A.tream....
(note, this search is limited assuming 'tream' is part of the file name, which is a bad assumption IMO however confirms at least two other impacted modules)
In s3fs we use
StreamWrapperManagerInterface::normalizeUri()(not static) and a couple static methods(called on the instance). The statics I could solve by moving off the object instance,normalizeUri()would require duplicating code (and again possible API change for s3fs as would have to remove the property from our base stream Wrapper class)Comment #9
longwaveThis is actually a bug introduced by #3571094: Remove RegisterStreamWrappersPass I think.
register()now instantiates the stream wrappers in order to get the class name, but it shouldn't actually do this.MR!16301 fixes this by injecting the class names as a separate argument, as there is no way to retrieve them from the service locator without instantiating them, reintroducing part of the compiler pass we previously removed. There is also a test with a stream wrapper that retrieves the stream wrapper manager in its constructor, which should prove that contrib stream wrappers can inject any service in their constructor.
Parts of this MR were assisted by Claude Code.
Comment #10
longwaveAfter this lands we should also be able to do #3583911: Clean up StreamWrapperManager::register() calls with no issues.
Comment #11
godotislateA couple nits on the MR.
I also ran the test-only job, and it failed:
I was thrown off initially because the test fails instead of erroring with an exception per the IS, but I believe that's because the test stream wrapper is getting the
stream_wrapper_manager, which is defined in the old container. So the missing service exception isn't thrown, but thestreamWrapperManagerobject in the test stream wrapper does not match the new stream wrapper manager.Maybe it would be more clear if the test module had a new service defined, with the test stream wrapper instantiating that new service in its constructor?
Comment #12
cmlaraQuick test of MR!16301 with s3fs and it does appear to allow tests to pass.
As an aside (not relevant to this issue, however worth Core team knowing from a design perspective):
I haven't been working on it it in some time haven't tested against 11.x at all, however in the s3fs 4.x series we would need to dynamically register streamWrappers, we couldn't expect them to be previously defined from a static list that would just be altered (as we do in the 3.x branch).
I'm not intendeding to imply any of these linked issues would actually break anything for us in 4.x I just want to encourage Core to keep in mind how early they bring streamWrappers in (ensure first compiler pass has always fully complete before the
stream_wrapper_managerservice is initialized). We have had to defend against core switching to service closures in the past cause issues with previously working code when the closure container differs from the final container.Comment #13
longwaveRe #12 are you registering them via a module service provider, or some other way? Out of scope here but if contrib has a pattern for this we can always test for it in core to avoid breaking it in future, especially as stream wrappers act a bit differently to other things due to needing to be registered with PHP itself.
Comment #14
cmlaraFor #13 (off topic to initial issue):
Yes a service provider, https://git.drupalcode.org/project/s3fs/-/blob/ef7d09953f5380b83ea1aa197...
Flysystem also does similar https://git.drupalcode.org/project/flysystem/-/blob/bc56a1f089f7a60f336e... (and theirs is in a stable release).
For context:
https://git.drupalcode.org/project/s3fs/-/commit/ef7d09953f5380b83ea1aa1... was the previous incident where service locators (closure with reduced container size) caused us issues (we were a second compiler pass to give us better access to Drupal Core services to load config).
I never raised that as a core issue as 4.x was/is pre-alpha and we found a workaround however from a design standpoint the latter a streamWrapper can register itself into the container the more UX and DX friendly it can be (and ideally everything during all the compiler passes is avoiding any stream access and is only working with raw files meaning late enrollment in container is a non-issue).
Comment #15
godotislatelgtm
Test only fails as expected: https://git.drupalcode.org/project/drupal/-/jobs/10918323.
I gave a shot at retitling this issue, though it's a bit long.
Comment #20
catchCommitted/pushed to main and cherry-picked to 11.x and 11.4.x, thanks!