Problem/Motivation
The FileUrlGeneratorInterface::generate() function accepts a string $uri and converts it into a URL object. The $uri can be in multiple formats, one of which being a stream wrapper. e.g. "public://path/to/my/file.jpg".
However, if the stream wrapper is for an external service (i.e. hosted outside of the current Drupal instance), then this doesn't work. The base path of the current site is prepended, and you end up with a url object that converts back to string like this: "http://www.mydrupalsite.com/https%3A//www.myexternalhost.com/path/to/my/file.jpg"
You can see here where base: is prepended to the uri string https://git.drupalcode.org/project/drupal/-/blob/3e4b17a7c9a926021202140...
elseif ($wrapper = $this->streamWrapperManager->getViaUri($uri)) {
// Attempt to return an external URL using the appropriate wrapper.
return Url::fromUri('base:' . $this->transformRelative(urldecode($wrapper->getExternalUrl()), FALSE));
}
Also see relevant Slack conversation about this issue: https://drupal.slack.com/archives/C1BMUQ9U6/p1639524487401900
Steps to reproduce
As there are no externally hosted stream wrappers as part of core, this can only be reproduced with a module.
We encountered the issue using flysystem_s3. With this module, files are stored using uri's like: s3://path/to/image.jpg and these are then converted into full S3 urls. E.g. https://s3-eu-west-2.amazonaws.com/my-s3-bucket-name/path/to/image.jpg.
Whilst there is a FileUrlGeneratorInterface::generateAbsoluteString(), which does work correctly with the use case above. This is a slightly different function, as it returns an absolute uri string, whereas FileUrlGeneratorInterface::generate() returns a url object.
Our issue is that there is code in core that calls FileUrlGeneratorInterface::generate(), such as in template_preprocess_file_link(). This was working with our remote stream wrappers fine before the FileUrlGenerator service was introduced.
This may also be an issue for the s3fs module (although this has not been confirmed yet).
Comments
Comment #2
leon kessler commentedAdding a patch that demonstrates the issue by adding an external hosted stream wrapper.
Comment #3
leon kessler commentedComment #4
berdirYes, one very awkward aspect of the refactor was that getExternalUrl() returns an external URL that we have to transform back, how about we add a check for UrlHelper::externalIsLocal().
This passes your test, only had a few minutes to throw it together, would be great if you could pick it up and deal with the dependency injection and BC for that and so on.
Ultimately, we want to replace getExternalUrl() with something that returns an Url object already, so we don't need to transform back and forth between URL object and string.
Comment #5
berdirMeant to upload a full patch.
We might also want to add some cases with query arguments and so on to make sure that works as well.
Comment #6
leon kessler commentedHuge thank you @Berdir for picking this up so quickly.
I've updated the patch with DI. I wasn't sure what the preferred method for BC is. Found this https://www.drupal.org/project/drupal/issues/3062100 so picked option 2 from there. I'm assuming the target for this is 9.4.x now.
I've also added tests for query strings and hashes. I had to update the line:
to
As otherwise the
$optionsaren't picked up in the outputted string.From what I can see (observing the tests and the flysystem_s3 module integration), there is only one call to getExternalUrl(), after this point it stays as a Url object outside of the stream wrapper.
But yes it would be nice if getExternalUrl() returned a url object.
Comment #7
leon kessler commentedNew patch fixing phpcs error.
Comment #9
leon kessler commentedWe have a secondary issue with our flysystem_s3 integration where url query parameters are being stripped out of the file link component.
It's related to this issue, but not exactly the same. So I've created a separate issue for it #3254727: File links with query parameters no longer work.
Comment #10
berdirAs a bugfix, this is still something that we want to backport I think, possibly without DI to keep the potential for possible conflicts to a minimum, but that could be done in a 9.3 backport patch.
test fails in #7 look random.
Comment #12
bladeduThere are some scenarios in which internal urls require to handle query parameters.
This patch propagate the $options in both cases.
Comment #13
bladeduI apologize, this is the good one. Previous patch missed a file.
Comment #14
cmlaraLooking at the error from #7 onwards I see
In my local lab.
Given it is still occurring and we did inject the request this may be a bit more than 'random'
It looks like that coming out of un-serilization from the page cache we don't have a request stack available to us.
Stacktrace:
NOTE: D9.3 as I haven't yet setup a D9.4 lab so line numbers may be slightly off.
Comment #15
berdirI've noticed that page cache unserialization thing through a form as well in some completely other context, that seems super weird to me that we end up with form object in internal page cache and something we should avoid as it makes page cache responses slower.
I'd suggest opening a separate issue for that, going back to the earlier patch that does not inject it with a @todo pointing to that other issue.
Comment #16
cmlaraIt appears (in this case) we end up with the form in the cached due to needing to work with callback information.
s:4:"ajax";a:2:{s:14:"edit-css-frame";a:7:{s:8:"callback";a:2:{i:0;O:37:"Drupal\ckeditor_test\Form\AjaxCssForm":8:{s:15:" * requestStack";N;s:16:" * configFactory";N;s:13:" * routeMatch";N;s:14:" * _serviceIds";a:2:{s:16:"fileUrlGenerator";s:18:"file_url_generator";s:17:"stringTranslation";s:18:"string_translation";}I can open an issue though I'm not really sure which component this should be filed against (form api, page_cache, or database cache backend?) and what to specify for a solution as I'm not sure what the alternatives for this could be.
Comment #17
berdirAh right, now I actually remember looking that up. And that again is part of the drupalSettings, right?
Because that makes absolutely no sense at all. We put all of #ajax into drupalSettings, but we should filter that out.
That's in \Drupal\Core\Render\Element\RenderElement::preRenderAjaxForm, we should remove that callback key in $settings. That could be a nice performance improvement on cached pages with ajax definitions.
That said, this won't guarantee that it can't happen in some other scenario, maybe in yet another issue we should make RequestContext more resilient when there is no request?
Comment #18
cmlaraThis likely is a good idea. Your comment reminded me that working on the s3fs StreamWrapper I had another issue with a different middleware (banmiddleware) that lead to a null request stack even when called inside of a module #3211523: HTTP middleware implementations can result in the Request object being null in S3fsStream.php.
Having hit that issue in s3fs I am a bit concerned that this single test failure in core is an indicator that more problems will occur with the request stack as code continues to adopt the new service.
I wonder if for this particular issue if we should do
as the the request_context, core StreamWrappers and s3fs module (not sure on flysystem) currently use this global value directly.
I can't point to any failures yet using
\Drupal::service('router.request_context')->getCompleteBaseUrl()and I know using the global is not ideal/best practice however in this case I wonder if we are likely to hit more issues by trying to force the use of the request stack now before we look into making it more resilient? Maybe I'm overreacting to suggest it however I at least wanted to throw it out there as a possibly 'safer' option.I am going to have to abstain from the drupalSettings question as it is outside my area of knowledge.
Comment #19
leon kessler commentedI've created a separate issue for the callback stored in the page_cache error. I don't fully understand it, so think of it more as a placeholder.
I will update the patch with suggestions from #15
Comment #20
leon kessler commentedHere's new patch. This includes using
\Drupal::service('router.request_context')as per #15, as well as the changes from #13.Comment #21
cmlaraPassed a few URL's direct through the service and they appeared to be generating correctly to me.
I haven't been able to think of a scenario yet that I could cause my concerns from #18 with the request stack so since I can't present proof of a problem this looks RTBC to me.
Comment #23
alexpottCommitted and pushed a60632076bf to 10.0.x and 7105aa278a8 to 9.4.x and 879b39ee5c5 to 9.3.x. Thanks!
Comment #27
ahmedabdelaty502 commentedthis patch to fix external files urls