Cropper won't work, response.uploadURL is null

Hi all,

I have been using Shrine inside Rails to successfully post images directly to S3 using Uppy for the frontend, and doing background jobs for derivatives, and using a derivation_endpoint for on-the-fly processing when a derivative isn’t yet available.

I have multiple uploaders working this way, and then tried to add image cropping to one of them using Cropper.JS, and the instructions on Shrine’s Wiki (Image Cropping · shrinerb/shrine Wiki · GitHub), but I can’t seem to get past this hiccup…

When I’m inside my form and choose an image to upload, I see Uppy places a thumbnail for it on the page, and Uppy’s upload progress bar fills up. But when the Progress Bar reaches the end, the image thumbnail disappears, and a broken file icon is shown in its place. The broken file icon has a NULL url (http://localhost:3000/company_profiles/1/null)

When I go to my fileUpload.js file, to the ‘uppy.on(‘upload-success’) function call, I discovered that the ‘response.uploadURL’ being passed to Cropper is null, so I suppose that’s why Cropper can’t do anything!

But I’m not sure how to troubleshoot ‘response.uploadURL’ being null….

Any thoughts? THANKS VERY MUCH!!!


const headshotUpload = (fileInput) => {
const hiddenInput = document.querySelector(’.headshot-upload-data’),
imagePreview = document.querySelector(’.headshot-preview img’),
formGroup = fileInput.parentNode
// remove our file input in favour of Uppy’s
const uppy = Core({
autoProceed: true,
restrictions: {
allowedFileTypes: [‘image/jpg’, ‘image/jpeg’, ‘image/heic’]
.use(FileInput, {
target: formGroup,
locale: { strings: { chooseFiles: ‘Choose file’ } },
.use(Informer, {
target: formGroup,
.use(ProgressBar, {
target: imagePreview.parentNode,
.use(ThumbnailGenerator, {
thumbnailWidth: 320,
thumbnailType: ‘image/jpeg’,
.use(AwsS3, {
companionUrl: ‘/’, // will call the presign endpoint on /s3/params
uppy.on(‘thumbnail:generated’, (file, preview) => {
// show preview of the image using URL from thumbnail generator
imagePreview.src = preview
uppy.on(‘upload-success’, (file, response) => {
// construct uploaded file data in the format that Shrine expects
const uploadedFileData = response.body[‘data’]
hiddenInput.value = JSON.stringify(uploadedFileData)

cropbox(imagePreview, response.uploadURL, {

  onCrop(detail) {
    let fileData = JSON.parse(hiddenInput.value)
    fileData['metadata']['crop'] = detail
    hiddenInput.value = JSON.stringify(fileData)


require ‘image_processing/vips’

class HeadshotUploader < Shrine
ALLOWED_TYPES = %w[image/jpeg image/heic].freeze
MAX_SIZE = 3 * 1024 * 1024
MAX_DIMENSIONS = [3000, 3000].freeze # 5000x5000
small: [320, 320],
medium: [600, 600],
large: [800, 800]

plugin :remove_attachment
plugin :pretty_location # creates a certain file structure on s3. Need to look it up.
plugin :validation_helpers
plugin :store_dimensions, log_subscriber: nil
plugin :derivation_endpoint, prefix: ‘derivations/headshot’

Attacher.derivatives do |original|
vips = ImageProcessing::Vips.source(original)
vips = vips.crop(*file.crop_points) # apply cropping
THUMBNAILS.transform_values do |(width, height)|
vips.resize_to_limit!(width, height)

begin crop code

Attacher.default_url do |derivative: nil, **|
next unless derivative && file

file.derivation_url :transform, shrine_class.urlsafe_serialize(
  crop: file.crop_points,
  resize_to_limit: THUMBNAILS.fetch(derivative)


derivation :transform do |file, transformations|
transformations = shrine_class.urlsafe_deserialize(transformations)
vips = ImageProcessing::Vips.source(file)

class UploadedFile
# convenience method for fetching crop points from metadata
def crop_points
metadata.fetch(‘crop’).fetch_values(‘x’, ‘y’, ‘width’, ‘height’)