I’m working on an app that uses DiskService for ActiveRecord storage. To debug issues, I copy the production database to development, but don’t want to download the blobs themselves. The code below shows how to serve a fallback image for images where the file is missing from the
Two parts of the ActiveStorage gem need to be monkeypatched:
When monkeypatching, it’s best to change the minimum amount of code, but since we’re trying to handle errors that are being rescued in the original code, we need to overwrite the original methods entirely. Had Rails handled errors in dedicated methods, we could have monkeypatched just those, but that is not the case.
The DiskController#show method rescues the
Errno::ENOENT error raised by Ruby when a given path does not exist in the filesystem.
This is the only line we need to change. Instead of returning
head :not_found, we tell it to serve a fallback image.
After applying this patch, any image absent from the filesystem will be replaced by
default_image.jpg instead of returning a 404.
If using blobs in non-web contexts, we also need to override the
In my case, I’m embedding images in PDFs, so I had to dive in.
Instead of raising
ActiveStorage::FileNotFoundError, we just return the contents of the
I don’t use streaming, otherwise I’d have to monkeypatch the stream method as well probably.
As before, we have to copy the whole method body just to change one line. Had Rails implemented a dedicated method for dealing with errors, this monkeypatch would be less hacky.
The tricky part happens when applying the patch: ActiveStorage only loads the DiskService if it is used (by defining
service: Disk in
config/storage.yml). When preparing the app, the
ActiveStorage::Service::DiskService constant is not yet available, so we need to force Rails to pick it up. After that, prepending our monkeypatch works just fine.
# lib/extras/active_storage_fallback.rb return unless Rails.env.development? module ActiveStorageControllerFallback def show if key = decode_verified_key serve_file named_disk_service(key[:service_name]).path_for(key[:key]), content_type: key[:content_type], disposition: key[:disposition] else head :not_found end rescue Errno::ENOENT serve_fallback end def serve_fallback default_image = Rails.root.join("app", "assets", "images", "image_unavailable.jpg") serve_file default_image, content_type: "image/jpg", disposition: :inline end end ActiveStorage::DiskController.prepend ActiveStorageControllerFallback module ActiveStorageServiceFallback def download(key, &block) if block_given? instrument :streaming_download, key: key do stream key, &block end else instrument :download, key: key do File.binread path_for(key) rescue Errno::ENOENT File.binread Rails.root.join("app", "assets", "images", "image_unavailable.jpg") end end end end # ActiveStorage does not autoload services, so we need to force reopening the class first module ActiveStorage; class Service::DiskService < Service; end; end ActiveStorage::Service::DiskService.prepend ActiveStorageServiceFallback
I’m putting monkeypatches in the
lib/extras folder, which is not autoloaded.
config.to_prepare, I tell Rails to load and execute every monkeypatch file.
# app/config/application.rb module YourAppName class Application < Rails::Application # Execute all monkey patches config.to_prepare do Rails.root.join("lib", "extras").children.each do |file| require file end end end end
There may be a cleaner way to achieve the same results but my goal here is just to simplify development, so I’m happy to stop here and move on to tasks that bring more value to the customer.