Bug report for Cloudinary Ruby SDK
Before proceeding, please update to latest version and test if the issue persists
Reproduced on 2.3.0; the relevant code is unchanged on current master (2.4.5).
Describe the bug in a sentence or two.
When Cloudinary is configured as the Active Storage service with a folder:, the integration's monkey-patch of CloudinaryHelper#cloudinary_url_internal treats every String source passed to cl_image_path/cl_image_tag/cloudinary_url as an Active Storage blob key and prepends the service folder — including strings that are already full Cloudinary public_ids, silently producing broken URLs like .../upload/v1/production/production/<id>.
Issue Type (Can be multiple)
Steps to reproduce
- Rails app with Active Storage configured to use Cloudinary, with a folder:
# config/storage.yml
cloudinary:
service: Cloudinary
folder: production
- In any view/helper, build a URL for an existing Cloudinary public_id (documented
cl_image_path usage, unrelated to Active Storage):
cl_image_path("production/abc123", type: :upload, version: 1)
- Expected:
https://res.cloudinary.com/<cloud>/image/upload/v1/production/abc123
Actual: https://res.cloudinary.com/<cloud>/image/upload/v1/production/production/abc123 (404)
The cause is in lib/active_storage/service/cloudinary_service.rb:
def cloudinary_url_internal(source, options = {})
service_instance, options = ActiveStorage::Service::CloudinaryService.fetch_service_instance_and_config(source, options)
service_instance ||= ActiveStorage::Blob.service
if defined?(service_instance.public_id) && options.fetch(:type, "").to_s != "fetch"
source = service_instance.public_id(source) # prepends @options[:folder] to ANY String
end
cloudinary_url_internal_original(source, options)
end
Three problems compound here:
- Action at a distance: merely configuring the Active Storage service changes the semantics of every helper call in the app. There is no way to tell from a call site that its String argument will be rewritten.
- The SDK already has the disambiguation mechanism and then bypasses it.
ActiveStorage::Blob#key is patched to return an ActiveStorage::BlobKey precisely so blob keys are distinguishable from plain strings, and fetch_service_instance_and_config checks for it — but the service_instance ||= ActiveStorage::Blob.service fallback then applies public_id(source) to plain Strings anyway. If the source is not a BlobKey, the safe inference is that it's a public_id and should be passed through untouched.
- No opt-out: the only escape is
type: :fetch (a side effect of the folder logic, not an API). There's no documented way to say "this string is already a complete public_id."
This broke ad-creative delivery in production for us: a helper rewrote upstream Cloudinary URLs into native delivery URLs on their source cloud — the parsed public_id (production/<id>) was correct, but the patch prepended our Active Storage folder a second time, 404ing every asset. It was invisible in CI because the test environment uses the Disk service (no public_id method → the patch is a no-op there).
Suggested fix: only apply service_instance.public_id(source) when source.is_a?(ActiveStorage::BlobKey) — or at minimum document that cl_image_path cannot be used with raw public_ids when the Active Storage service is configured, and document Cloudinary::Utils.cloudinary_url as the unpatched alternative.
Error screenshots or Stack Trace (if applicable)
N/A — silent wrong output (404 URLs), no error raised.
Operating System
Environment and Libraries (fill in the version numbers)
- Cloudinary Ruby SDK version - 2.3.0 (code unchanged on master / 2.4.5)
- Ruby Version - 3.4.7
- Rails Version - 8.1.3
- Other Libraries (Carrierwave, ActiveStorage, etc) - ActiveStorage (Rails 8.1.3)
Repository
N/A — the three-line repro above plus the quoted SDK source fully characterizes the issue.
Bug report for Cloudinary Ruby SDK
Before proceeding, please update to latest version and test if the issue persists
Reproduced on 2.3.0; the relevant code is unchanged on current master (2.4.5).
Describe the bug in a sentence or two.
When Cloudinary is configured as the Active Storage service with a
folder:, the integration's monkey-patch ofCloudinaryHelper#cloudinary_url_internaltreats every String source passed tocl_image_path/cl_image_tag/cloudinary_urlas an Active Storage blob key and prepends the service folder — including strings that are already full Cloudinary public_ids, silently producing broken URLs like.../upload/v1/production/production/<id>.Issue Type (Can be multiple)
Steps to reproduce
cl_image_pathusage, unrelated to Active Storage):https://res.cloudinary.com/<cloud>/image/upload/v1/production/abc123Actual:
https://res.cloudinary.com/<cloud>/image/upload/v1/production/production/abc123(404)The cause is in
lib/active_storage/service/cloudinary_service.rb:Three problems compound here:
ActiveStorage::Blob#keyis patched to return anActiveStorage::BlobKeyprecisely so blob keys are distinguishable from plain strings, andfetch_service_instance_and_configchecks for it — but theservice_instance ||= ActiveStorage::Blob.servicefallback then appliespublic_id(source)to plain Strings anyway. If the source is not aBlobKey, the safe inference is that it's a public_id and should be passed through untouched.type: :fetch(a side effect of the folder logic, not an API). There's no documented way to say "this string is already a complete public_id."This broke ad-creative delivery in production for us: a helper rewrote upstream Cloudinary URLs into native delivery URLs on their source cloud — the parsed public_id (
production/<id>) was correct, but the patch prepended our Active Storage folder a second time, 404ing every asset. It was invisible in CI because the test environment uses the Disk service (nopublic_idmethod → the patch is a no-op there).Suggested fix: only apply
service_instance.public_id(source)whensource.is_a?(ActiveStorage::BlobKey)— or at minimum document thatcl_image_pathcannot be used with raw public_ids when the Active Storage service is configured, and documentCloudinary::Utils.cloudinary_urlas the unpatched alternative.Error screenshots or Stack Trace (if applicable)
N/A — silent wrong output (404 URLs), no error raised.
Operating System
Environment and Libraries (fill in the version numbers)
Repository
N/A — the three-line repro above plus the quoted SDK source fully characterizes the issue.