React Blob to File upload via Node endpoint – file is corrupt

I’ve built a Slack-style avatar image upload and crop feature, and I can’t get the cropped image to save without corruption. The original file upload, using the same endpoint and method, works just fine. It’s the cropped copy, created manually from a Blob, that is always corrupt.

Steps are pretty simple:

  1. Select and upload an image file
  2. Crop (react-image-crop) appears
  3. Select area, hit Save

In Step 1, the file is uploaded when the file input changes. File is sent to a streaming endpoint which uploads the file to an s3 bucket.

<button type="button">Upload</button>
<input 
    type="file" 
    accept="image/*"
    onChange={onFileChange} 
/>

...

const onFileChange = async e => {
    e.preventDefault();
    let uploadedFile = e.target.files[0];
    await onSave(uploadedFile);
};

The uploadedFile var is the File object returned from the input control. This works great! No issues, yet.

enter image description here

In Step 3, once you’ve selected an area of the image, a Blob is produced by react-image-crop.

const getCroppedImage = (source, config) => {
    const canvas = document.createElement("canvas");
    const scaleX = source.naturalWidth / source.width;
    const scaleY = source.naturalHeight / source.height;
    canvas.width = config.width;
    canvas.height = config.height;
    const ctx = canvas.getContext("2d");

    ctx.drawImage(
        source,
        config.x * scaleX,
        config.y * scaleY,
        config.width * scaleX,
        config.height * scaleY,
        0,
        0,
        config.width,
        config.height
    );

    let mimeType = mime.lookup(userProfile.image_file.split(".").at(-1));

    return new Promise((resolve, reject) => {
        canvas.toBlob(blob => {
            if (!blob) {
                reject(new Error("Canvas is empty"));
                return;
            }
            resolve(blob); //***THIS BLOB...
        }, mimeType);
    });
};

This Blob is valid, because I display the selected area on the screen before saving:

const AvatarPreview = () => {
    if (activeAvatar) {
        return <ImageCropper imageToCrop={activeAvatar} onImageCropped={onImageCropped} />;
    }
    return <Icon icon="bi:person" />;
};

I stuff the Blob produced by react-image-crop, into a File object because that’s what my code expects, just like Step 1.

const onImageCropped = croppedBlob => { //***IS PASSED IN HERE
    let croppedImg = URL.createObjectURL(croppedBlob);
    setActiveAvatar(croppedImg);
    const reader = new FileReader();
    reader.readAsDataURL(croppedBlob);
    reader.addEventListener("load", () => {
        let { result } = reader;
        let resultMimeType = result.split(";")[0].split(":")[1];
        let croppedFile = new File([result], userProfile.image_file, { type: resultMimeType }); //***NEW File object, from Blob
        setCroppedAvatar(croppedFile);
    }, false);
};

<button type="button" onClick={() => onSave(croppedAvatar)}>Save</button>

The “result” in the FileReader load above is the base64 image data:

'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA…QCMFWeFZ9u3fPsWY3hh9uVHWllbSwtGASrIOQc1t6rt0OBqG6x7aVfdY859pnIwfDDhCfpwBUyaSus4VZO+susGGM60zrB1TuEjx5Ov76FBKUgNTltIQlP4UpQghKQMAAAADHApt6lv1x1HeHrzdny9KkJbDrpHLhQhKNx91EJBJ9SSaQkLS+4HTwvalKvmQMZ/PFa8hBCRntnirYUDqVYtEWlQ8JYJSrv8vnTy6W2h5/qBYmCgqbcmtgLSMg+YUyYD4aUUqTlJ4I+td+FLethRhai0sgoUOCg+hFPBwcxjruUie1k/VDOmenKkIcS2iJCzyeAAmvF7qjqFzVWurzfHV7jJluKBJ/hBwP6VL1u+KvqlD0lL0VeLt+2rZLjmO25MyqSwNuPK7nKh8l7vlioFejqU8tx453EnIPcmrWrvW5VFf6zK0GifTWs1n6TRoUrIaDZBSeDSVUzyMTY7gBIORR/EcXwTnFEozfc0xVwOYT/2Q=='

The new File object seems legit to me:

enter image description here

I then send the image through the endpoint again, and it’s uploaded. It appears in the bucket, and the file size seems legit (not 0KB like a broken stream would indicate.)

However, upon downloading and attempting to open the file, it’s corrupt. I guess I’m missing an option somewhere…some little tweak that would make this work? Is the File object not formed correctly? How do I troubleshoot this further?