Skip to content

Active Storage integration silently rewrites public_ids passed to cl_image_path/cloudinary_url app-wide (folder prepended twice) #594

@brendanmatkin

Description

@brendanmatkin

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)

  • Behaviour - Functions are not working as expected (such as generate URL)
  • Documentation - Inconsistency between the docs and behaviour

Steps to reproduce

  1. Rails app with Active Storage configured to use Cloudinary, with a folder:
    # config/storage.yml
    cloudinary:
      service: Cloudinary
      folder: production
  2. 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)
  3. 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:

  1. 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.
  2. 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.
  3. 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

  • All

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions