Problem/Motivation
When the built-in private:// scheme is overridden with a remote Flysystem adapter (AWS S3 / aws_s3, visibility private), Drupal generates download and image-style URLs that 404.
FlysystemStreamWrapper::getExternalUrl() always emits:
/flysystem/files/{scheme}?file={urlencoded-target}
for any scheme with private visibility — including the built-in private scheme.
FlysystemRouteSubscriber explicitly skips registering /flysystem/files/private for that scheme, on the assumption that core's /system/files + FileDownloadController will serve the files.
Those two halves do not match:
- The generated URL never hits core's route.
- Core's
FileDownloadControllerusesBinaryFileResponse, which needs a local path. It cannot stream a remote S3 object. - Filenames with spaces/commas break core's
{filepath}path matching — the?file=query param exists precisely to avoid that.
Observed on Flysystem 3.0.0-beta2 (Drupal 11). Media edit form with an image widget (preview_image_style: large) shows a broken preview even though the object exists in the private bucket and file_exists('private://…') returns TRUE.
Example URL Drupal emits:
/flysystem/files/private?file=prensa%2FCOORTE%2C%20Adriaen_Still%20Life%20with%20Wild%20Strawberries_Inv.1106_repro.jpg
router.route_provider reports flysystem.private_file.private as missing. Browser gets 404. Image-style URLs fail the same way:
/flysystem/files/private?file=styles%2Flarge%2Fprivate%2Fprensa%2F…
Steps to reproduce
- Override
private://in settings with a remote adapter, e.g.aws_s3,visibility: private. - Create (or migrate) a managed file at
private://some/path/file with spaces, comma.jpg. - Confirm the object exists in the bucket and
file_exists($uri)is TRUE. - Attach it to a media/image field and open the entity form (preview image style enabled).
- Inspect the preview
<img src>or$file->createFileUrl().
Expected
The URL resolves to FlysystemPrivateFileController::serveFile, which streams the original (and generates image-style derivatives on demand).
Actual
404. Route flysystem.private_file.private is never registered. Custom private schemes (secure-media, etc.) work; only the built-in private override is skipped.
Proposed resolution
Stop special-casing $scheme === 'private' in FlysystemRouteSubscriber::alterRoutes(). Register /flysystem/files/private the same way as any other private-visibility scheme.
That matches what getExternalUrl() already emits, lets FlysystemPrivateFileController generate derivatives, and handles special characters in filenames via ?file=.
Core's /system/files routes can stay; they are simply unused for these URLs.
Invert FlysystemRouteSubscriberTest::testPrivateSchemeOverrideDoesNotAddFlysystemRoute() so it asserts the route is registered (path, _disable_route_normalizer, no public image-style route).
Patch against 3.0.x (applies on 3.0.0-beta2 after existing empty-directory-path route changes):
diff --git a/src/Routing/FlysystemRouteSubscriber.php b/src/Routing/FlysystemRouteSubscriber.php
--- a/src/Routing/FlysystemRouteSubscriber.php
+++ b/src/Routing/FlysystemRouteSubscriber.php
@@ -144,17 +147,11 @@ protected function alterRoutes(RouteCollection $collection): void {
if ($definition->visibility === 'private') {
- if ($scheme === 'private') {
- // Core's /system/files route handles private:// downloads.
- // FlysystemFileSystem handles the file operations transparently.
- continue;
- }
-
$collection->add(
'flysystem.private_file.' . $scheme,
Happy to attach a full patch including the unit test and docblock updates.
Remaining notes (out of this issue's scope)
FlysystemPrivateFileController::deliverFile() does not invoke hook_file_download(). Access is owner / administer files / bypass file access only. Image-style derivatives go through deliverDerivative() with no entity access check. Separate from this 404, but worth a follow-up if core-compatible private access is desired.
| Comment | File | Size | Author |
|---|---|---|---|
| flysystem-private-scheme-download-route.patch | 5.95 KB | luigisa |
Issue fork flysystem-3618271
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:
Comments
Comment #6
lisa.rae commentedComment #8
lisa.rae commented