Skip to content

Race condition when uploading images with automaticallyOpenImageEditorForAspectRatio #19808

Description

@puzzledmonkey

Package

filament/forms

Package Version

v5.5.1

Laravel Version

v13.5.0

Livewire Version

No response

PHP Version

PHP 8.3.6

Problem description

When using the automaticallyOpenImageEditorForAspectRatio mode for FileUploads, it's possible to trigger a race condition where the file field ends up containing 2 values, even when isMultiple is false, maxFiles is set to 1, and maxParallelUploads is set to 1.

The 2 values contain both the original and cropped versions of the image. The root cause seems to be due to being able to trigger the cropped 'upload' before the original (uncropped) upload has completed. This in turn means that the 'removeUploadedFile' call (to remove the original file) is never triggered by the cropper since it is as yet unaware of the upload, and hence the field ends up with 2 files attached.

I've had a quick look through the code layout and but as I'm a newly-minted Filament fan I'm not sure what the correct solution is here. Ideally from a usability point of view, if automaticallyOpenImageEditorForAspectRatio is set and the user cancels the cropper, than nothing should be uploaded at all. To do this it looks like the 'addfile' listener on FilePond in filament/forms file-upload.js line 431 or so is the general area, but I couldn't see a way to hook that and abort the filepond file upload. Of course we need the addfile listener to get the image mime type and dimensions, so we cannot abort earlier.

As a second option - and again, I couldn't see a simple way to do this, sorry - the hook to open the cropper automatically could be attached after the first upload finishes entirely (filepond seems to indicate this is the 'processfile' event but in my tests that still seemed to fire before the upload call returned?). In that case, it would be a nice touch (if possible) to have the cropper call the removeUploadedFile function if the user cancels the cropper, since we do not want the original, uncropped, image to be submittable at all.

I've attached the response to the final call to _finishUpload to clarify how the 2 values are attached.

Let me know if there's anything I can help clarify?

PS thanks so much for Filament, it's an absolute lifesaver.

Expected behavior

Only the final cropped image should be stored in the field value.

Steps to reproduce

  1. Create a FileUpload field with automaticallyOpenImageEditorForAspectRatio set to true (you'll need to configure an aspect ratio too)
  2. Open your browser dev tools on the network tab as it makes the timing a lot easier
  3. Start the image upload with any image
  4. The image cropper should open very quickly
  5. Shortly after that you'll see a network request to 'update' fire off (this is the '_startUpload' call)
  6. As soon as you see that network request fire, press 'Save' on the image cropper.
  7. Wait for all network requests to complete
  8. Note the lack of 'removeUploadedFile' call before the cropper starts, and the 2 values for the field in the response to the final '_finishUpload' call

Reproduction repository (issue will be closed if this is not valid)

https://github.com/puzzledmonkey/filament-upload-problem

Relevant log output

{"_token":"RUNNhRC6rxyzVuzBh9wIPySqLmGZCjTIQlGty0NQ","components":[{"snapshot":"{\"data\":{\"record\":null,\"data\":[{\"image\":[{\"1e2859d2-7e84-47f6-b296-0ed5da1de951\":[\"livewire-file:1lTpicBpNX2w5D7BHdKyi4RCZQApw9vEHeiQZ4Ap.jpg\",{\"s\":\"fil\"}]},{\"s\":\"arr\"}]},{\"s\":\"arr\"}],\"previousUrl\":\"https:\\/\\/laravel.localhost\\/admin\\/tests\",\"isCreating\":false,\"mountedActions\":[[],{\"s\":\"arr\"}],\"defaultAction\":null,\"defaultActionArguments\":null,\"defaultActionContext\":null,\"defaultTableAction\":null,\"defaultTableActionRecord\":null,\"defaultTableActionArguments\":null,\"componentFileAttachments\":[[],{\"s\":\"arr\"}],\"areSchemaStateUpdateHooksDisabledForTesting\":false,\"discoveredSchemaNames\":[[\"form\",\"content\",\"headerWidgets\",\"footerWidgets\"],{\"s\":\"arr\"}],\"parentRecord\":null,\"savedDataHash\":null},\"memo\":{\"id\":\"09KWnf9K0lpfulu2qgN3\",\"name\":\"App\\\\Filament\\\\Resources\\\\Tests\\\\Pages\\\\CreateTest\",\"path\":\"admin\\/tests\\/create\",\"method\":\"GET\",\"release\":\"a-a-a\",\"children\":[],\"scripts\":[],\"assets\":[],\"errors\":[],\"locale\":\"en\",\"islands\":[]},\"checksum\":\"49b9788efa36f9e82b900579d72a6458f7b7b42de060b2814118a73abb35735c\"}","updates":{},"calls":[{"method":"_finishUpload","params":["data.image.5041b9ed-9c34-4d47-a66e-61e20a8d2fb1",["d2fe10e9:/E6VwjnZuVRT6FkDwGaZ2rMNQvHLBXt4YNXOf6YvF.jpg"],false,false],"metadata":{}}]}]}

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions