Bug description
Statamic\Eloquent\Assets\AssetRepository::findByUrl() looks up the asset path without decoding it first, unlike the core repository it replaces. Asset URLs are percent-encoded, so any asset whose filename contains a character that gets encoded (spaces, parentheses, @, accented characters, commas) cannot be resolved from its own URL and returns null.
Expected: Asset::find($asset->url()) returns the asset it was given.
Actual: it returns null for any filename that needs URL encoding.
The cause is a single missing call. Core (statamic/cms, src/Assets/AssetRepository.php:62):
$path = rawurldecode(Str::after($url, $containerUrl));
Eloquent driver (src/Assets/AssetRepository.php:56):
$path = Str::after($url, $containerUrl);
Without the decode, findById() is handed my%20file%20%281%29.jpg and queries the database for that literal basename, which matches no row.
In our production install this made every page carrying such an asset as its Open Graph image return a 500. See "Additional details" for why a failed lookup escalates that far.
How to reproduce
- Enable the eloquent driver for assets.
- Upload an asset with a space, parenthesis or
@ in its filename, e.g. my file (1).jpg.
- Resolve that asset from its own URL:
$asset = Asset::find('assets::my file (1).jpg');
$asset->url(); // "/assets/my%20file%20%281%29.jpg"
Asset::find($asset->url()); // null <- expected the same asset back
Any filename that survives a round trip through url() unencoded works fine, which is why this stays hidden until someone uploads a file with a space in it.
To see it take down a page, put such an asset in SEO Pro's Open Graph image field and request any page that uses it.
Logs
Call to a member function isVideo() on null
Error: Call to a member function isVideo() on null at /var/www/html/vendor/statamic/cms/src/Imaging/ImageGenerator.php:158
#0 /var/www/html/vendor/statamic/cms/src/Tags/Glide.php(180): Statamic\Imaging\ImageGenerator->generateByAsset()
#1 /var/www/html/vendor/statamic/cms/src/Tags/Glide.php(139): Statamic\Tags\Glide->generateImage()
#2 [internal function]: Statamic\Tags\Glide->{closure:Statamic\Tags\Glide::generate():134}()
#5 /var/www/html/vendor/statamic/cms/src/Tags/Glide.php(134): Illuminate\Support\Collection->map()
#6 [internal function]: Statamic\Tags\Glide->generate()
#17 /var/www/html/vendor/statamic/seo-pro/src/RendersMetaHtml.php(17): Illuminate\View\View->render()
#18 /var/www/html/vendor/statamic/seo-pro/src/Tags/SeoProTags.php(31): Statamic\SeoPro\Tags\SeoProTags->renderMetaHtml()
#30 /var/www/html/vendor/statamic/cms/src/Http/Responses/DataResponse.php(155): Statamic\View\View->render()
#32 /var/www/html/vendor/statamic/cms/src/Entries/Entry.php(614): Statamic\Http\Responses\DataResponse->toResponse()
Environment
Environment
Laravel Version: 12.68.0
PHP Version: 8.5.2
Composer Version: 2.10.2
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: UTC
Locale: nl
Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: CACHED
Drivers
Broadcasting: log
Cache: file
Database: mysql
Logs: stack / single
Mail: log
Queue: sync
Session: file
Storage
public/storage: LINKED
Statamic
Addons: 7
License Key: Not set
Sites: 19
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 6.29.0 PRO
Statamic Addons
appswithlove/statamic-one-click-content-translation: 6.0.6
eminos/statamic-tabs: 2.0.1
keyagency/content-importer: dev-main
keyagency/kai-personalize: 1.2.12
keyagency/statamic-asset-usage: 1.0.1
statamic/eloquent-driver: 5.11.0
statamic/seo-pro: 7.13.3
Statamic Eloquent Driver
Addon Settings: eloquent
Asset Containers: file
Assets: eloquent
Blueprints: eloquent
Collection Trees: eloquent
Collections: eloquent
Entries: eloquent
Fieldsets: file
Form Submissions: eloquent
Forms: eloquent
Global Sets: eloquent
Global Variables: eloquent
Navigation Trees: eloquent
Navigations: eloquent
Revisions: eloquent
Sites: eloquent
Taxonomies: eloquent
Terms: eloquent
Tokens: eloquent
Additional details
Statamic\Tags\Glide::generateImage() passes the result straight through:
return $this->getGenerator()->generateByAsset(Asset::find($item), $params);
ImageGenerator::generateByAsset() has no null check, so line 158 throws Error: Call to a member function isVideo() on null. Because that is an \Error and Glide::generate() only catches \Exception, it escapes the tag's own error handling and takes down the entire response instead of just skipping the image.
The path that reaches this in practice is an Asset instance passed to the glide tag: normalizeItem() returns it unchanged, and Asset::find() then casts it to a string via Asset::__toString(), which yields the encoded url(). SEO Pro's meta.antlers.html does exactly this for og:image, so a single asset with a space in its name 500s every page that uses it. Static caching never stores the error response, so it recurs on every request.
The missing null check in ImageGenerator is arguably a second bug, but it lives in statamic/cms rather than in this repository.
Suggested fix
Restore the decode to match core:
- $path = Str::after($url, $containerUrl);
+ $path = rawurldecode(Str::after($url, $containerUrl));
We are running that change as a local subclass of the repository and it resolves all affected assets, with no other behaviour changing.
Bug description
Statamic\Eloquent\Assets\AssetRepository::findByUrl()looks up the asset path without decoding it first, unlike the core repository it replaces. Asset URLs are percent-encoded, so any asset whose filename contains a character that gets encoded (spaces, parentheses,@, accented characters, commas) cannot be resolved from its own URL and returnsnull.Expected:
Asset::find($asset->url())returns the asset it was given.Actual: it returns
nullfor any filename that needs URL encoding.The cause is a single missing call. Core (
statamic/cms,src/Assets/AssetRepository.php:62):Eloquent driver (
src/Assets/AssetRepository.php:56):Without the decode,
findById()is handedmy%20file%20%281%29.jpgand queries the database for that literalbasename, which matches no row.In our production install this made every page carrying such an asset as its Open Graph image return a 500. See "Additional details" for why a failed lookup escalates that far.
How to reproduce
@in its filename, e.g.my file (1).jpg.Any filename that survives a round trip through
url()unencoded works fine, which is why this stays hidden until someone uploads a file with a space in it.To see it take down a page, put such an asset in SEO Pro's Open Graph image field and request any page that uses it.
Logs
Environment
Additional details
Statamic\Tags\Glide::generateImage()passes the result straight through:ImageGenerator::generateByAsset()has no null check, so line 158 throwsError: Call to a member function isVideo() on null. Because that is an\ErrorandGlide::generate()only catches\Exception, it escapes the tag's own error handling and takes down the entire response instead of just skipping the image.The path that reaches this in practice is an
Assetinstance passed to the glide tag:normalizeItem()returns it unchanged, andAsset::find()then casts it to a string viaAsset::__toString(), which yields the encodedurl(). SEO Pro'smeta.antlers.htmldoes exactly this forog:image, so a single asset with a space in its name 500s every page that uses it. Static caching never stores the error response, so it recurs on every request.The missing null check in
ImageGeneratoris arguably a second bug, but it lives instatamic/cmsrather than in this repository.Suggested fix
Restore the decode to match core:
We are running that change as a local subclass of the repository and it resolves all affected assets, with no other behaviour changing.