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:

  1. The generated URL never hits core's route.
  2. Core's FileDownloadController uses BinaryFileResponse, which needs a local path. It cannot stream a remote S3 object.
  3. 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

  1. Override private:// in settings with a remote adapter, e.g. aws_s3, visibility: private.
  2. Create (or migrate) a managed file at private://some/path/file with spaces, comma.jpg.
  3. Confirm the object exists in the bucket and file_exists($uri) is TRUE.
  4. Attach it to a media/image field and open the entity form (preview image style enabled).
  5. 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.

Issue fork flysystem-3618271

Command icon 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

luigisa created an issue. See original summary.

lisa.rae made their first commit to this issue’s fork.

  • 5b1b2bee committed on 3.0.x
    Issue #3618271 by luigisa, lisa.rae: Expanding tests to fill a testing...

  • luigisa authored 5969de16 on 3.0.x
    Issue #3618271 by luigisa: Register /flysystem/files/private for...
lisa.rae’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

lisa.rae’s picture

Status: Fixed » Closed (fixed)