antiwork/gumroad · error · DisputeEvidence::MergeCustomerCommunicationFilesService::MergeError

One of the uploaded files is not a JPG, PNG, or PDF.

Error message

One of the uploaded files is not a JPG, PNG, or PDF.

What it means

Every input blob's content_type must be in DisputeEvidence::ALLOWED_FILE_CONTENT_TYPES (image/jpeg, image/png, application/pdf) or MergeError is raised. Because the merged output is always application/pdf, the model-level content-type validation never inspects the raw inputs — this explicit check is the only gate before seller-supplied bytes reach ImageMagick and qpdf.

Source

Thrown at app/services/dispute_evidence/merge_customer_communication_files_service.rb:50

  def self.perform(blobs:, max_size:)
    new(blobs:, max_size:).perform
  end

  def initialize(blobs:, max_size:)
    @blobs = blobs
    @max_size = max_size
  end

  # Returns a new application/pdf ActiveStorage::Blob. The input blobs are left alone: the
  # caller purges them once the submission has actually been persisted.
  def perform
    if blobs.size > DisputeEvidence::MAX_CUSTOMER_COMMUNICATION_FILES
      raise MergeError, "You can attach up to #{DisputeEvidence::MAX_CUSTOMER_COMMUNICATION_FILES} files."
    end
    # The merged output is always application/pdf, so the model's content-type validation never
    # sees the inputs. Everything below hands seller-supplied bytes to ImageMagick and qpdf.
    unless blobs.all? { _1.content_type.in?(DisputeEvidence::ALLOWED_FILE_CONTENT_TYPES) }
      raise MergeError, UNSUPPORTED_FILE_TYPE_MESSAGE
    end
    raise FilesTooLargeError, FILE_TOO_LARGE_MESSAGE if blobs.any? { _1.byte_size > max_size }

    downloaded_files = download_blobs
    merged_path = merge_within_size_budget(downloaded_files)

    File.open(merged_path) do |file|
      ActiveStorage::Blob.create_and_upload!(io: file, filename: MERGED_FILENAME, content_type: "application/pdf")
    end
  ensure
    downloaded_files&.each { _1[:tempfile].close! }
    File.unlink(merged_path) if merged_path && File.exist?(merged_path)
  end

  private
    attr_reader :blobs, :max_size

    def download_blobs

View on GitHub (pinned to afeacbd394)

Solutions

  1. Convert HEIC/WEBP/GIF/TIFF to plain JPG or PNG with a real converter before uploading
  2. Re-export documents as PDF
  3. If a previously saved attachment is the offender, remove it and submit a converted version

Example fix

# before
# photo.HEIC uploaded directly from iPhone Photos
MergeCustomerCommunicationFilesService.perform(blobs: [heic_blob], max_size: budget)
# => MergeError: One of the uploaded files is not a JPG, PNG, or PDF.

# after
# 'sips -s format jpeg photo.HEIC --out photo.jpg' (macOS) or any converter, then upload
Defensive patterns

Strategy: validation

Validate before calling

unless blobs.all? { _1.content_type.in?(DisputeEvidence::ALLOWED_FILE_CONTENT_TYPES) }
  raise 'only JPG, PNG and PDF are accepted'
end

Type guard

def all_customer_communication_files_allowed?(blobs)
  blobs.all? { _1.content_type.in?(DisputeEvidence::ALLOWED_FILE_CONTENT_TYPES) }
end

Try / catch

begin
  DisputeEvidence::MergeCustomerCommunicationFilesService.perform(blobs:, max_size: budget)
rescue DisputeEvidence::MergeCustomerCommunicationFilesService::MergeError => e
  redirect_back alert: e.message
end

Prevention

When it happens

Trigger: Attaching a HEIC photo (iPhone default for 'Photos' exports), a WEBP screenshot (many modern capture tools), GIF, TIFF, or any file whose recorded ActiveStorage content_type is outside the allowlist. The previously attached blob also counts as an input, so an older disallowed attachment re-triggers it on a later save.

Common situations: iPhone photos dragged out of the Photos app stay HEIC; screenshot/browser tools increasingly emit WEBP; files renamed '.jpg' without conversion keep their original recorded content type.

Related errors


AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21). Data as JSON: /api/errors/c2147b9ca302eea3. Report an issue: GitHub.