rails: ActiveStorage: invalid Attachment when last step of direct upload fails
I am trying to handle the case where the last step of a direct upload fails (the PATCH
call to my model), and I have noticed an odd behaviour. Here goes:
Steps to reproduce
- Set up the
Profile
model withhas_one_attached :document
. - Create an instance of
Profile
(referred to asprofile
) - Use a direct upload form to attach a document to
profile
(controller and view code below) - Everything works fine so far, I can see the image and its filename.
- Force a failure in the
PATCH
call to/profile/document
: intentionally modify a validation in the Profile model so that any call to Profile#update fails - Upload another document for the same Profile, and watch the
PATCH
fail.
Expected behavior
When this last step fails, I’d expect the whole state of my app to go back to its state from before the beginning of the upload process (the POST
call to rails/active_storage/direct_uploads
).
I’d also expect the former file to still be there on the storage, and the new one to not be there.
Actual behavior
The Attachment record for my Profile is still the same, but the original Blob was destroyed, and replaced by a new one (created by the first POST
to rails/active_storage/direct_uploads
). The Attachment points to the destroyed Blob.
profile.document.attached?
returns true
, but other methods, e.g. filename
or service_url
are undefined. This breaks my views: the if passes, the statements inside break.
S3/Disk contains the new file, and does not contain the former one.
The logs tell me that the first call from the client to /rails/active_storage/direct_uploads
creates a Blob. Then, after the upload to S3 is done, the PATCH
call to /profile/document
destroys the former Attachment and creates a new one, but this is all rolled back when my validation fails. However, it also enqueues a job that purges the former Blob, instead of purging the newly created one.
From my understanding, what this job does should depend upon the success of the PATCH
call. Is this an actual bug, or am I supposed to handle this manually? Or maybe there’s just another way to write things and make it work automagically?
One workaround I found is to call purge or detach on the document when the update fails. From a user’s point of view it removes the former document, which isn’t so nice, but at least my views don’t break. The Attachment is destroyed, the Blob remains, the former file is destroyed, the new one is stored. This fix is commented out in the code sample.
System configuration
I get the bug on my local environment, both with S3 and Disk as targets.
Rails version: Rails 5.2.0.rc1
Ruby version: ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin17]
Code samples
Adapted for better readability.
class ProfilesController < ApplicationController
def update
if current_user.profile.update(document_params)
flash[:notice] = "Your document was successfully uploaded"
else
# current_profile.reload.identity_document.purge # The fix
flash[:alert] = "Something went wrong."
end
redirect_to my_form_url
end
private
def document_params
params.require(:profile).permit(:document)
end
end
<% if @profile.document.attached? %>
<%= @profile.document.filename %>
<%= image_tag @profile.document %>
<% else %>
No document yet
<% end %>
<%= form_for @profile do |f| %>
<%= f.file_field :document, direct_upload: true %>
<%= f.submit %>
<% end %>
Thanks in advance for your help!
About this issue
- Original URL
- State: open
- Created 6 years ago
- Reactions: 12
- Comments: 16 (4 by maintainers)
I found a similar issue (not using direct upload). I was going to raise it as a new issue, until I read yours. I’d already written up the whole issue report, so I’ve included it here. I feel they are caused by the same issue. I think my steps reproduce the issue in a simple way.
Attachment without Blob on update of record with validation error
When a record fails validation at the same time as updating an attachment, the old Blob is deleted and the Attachment exists in an orphaned state without its parent Blob.
Steps to reproduce
rails new
.rails active_storage:install
.rails g scaffold user name:string
.rails db:migrate
.app/views/users/_form.html.erb
and add a basic file upload field::avatar
to permitted params inapp/controllers/users_controller.rb
:name
andavatar
(any file).name
(as expected). See log below.Expected behaviour
I would expect that due to a validation error on updating the User, the attached avatar should not change and remain as the original file (Blob with key
XdCRghPAcU721RtB7ERgAjAT
uploaded in step 9).Inspecting the User record (expected):
Actual behavior
The transaction in step 11 rolled back reverting the User record and Attachment record. However, a PurgeJob still runs deleting the original Blob (key
XdCRghPAcU721RtB7ERgAjAT
).The new attachment (uploaded in step 11) is added to storage with a key of
duahAp6ATEsrAmnP2Ax5frLA
and remains on disk but no Blob record exists pointing to this file.Inspecting the User record (actual):
System configuration
Rails version: 5.2.0.rc2
Ruby version: 2.4.2
Log from step 9
Log from step 11
Good thing it’s almost 2020 and I’m still running into this 🤦♂
It’s now 2021…
Helloooo 2022!
It’s the end of 2020…
It’s the middle of 2020, but I’m still stuck by this.
Instead of reciting to us what year it is, how about providing additional detail about the situation that may have been missing from this thread? 🙇
“Me too” comments are unhelpful, which is why GitHub added the emoji reactions. This does help us identify issues that are impacting more people than we may realize. I don’t want to invalidate your experiences, I know it can be frustrating to end up on a GH issue that is several years old. 🙏
The last productive comment was nearly 5 years ago, which references multiple workarounds and even a fix landing in Rails 6.
Are the people experiencing this issue still on Rails 5? Have you tried the various workarounds mentioned here? Can you provide an executable test case against
main
?It’s the end of 2021… I’m here
I have the same issue as @dtcristo. Apparently it is fixed in Rails 6 (https://github.com/rails/rails/issues/32449#issuecomment-404938746), but still very annoying now.
The workaround suggested by @SimonFrr doesn’t work for me. The file remains corrupted. It seems like it isn’t purged when the update fails (when there is no save performed due to a validation error).
A workaround would be to check everywhere you use the file is to check whether the blob is present (
model.upload.blog.present?
), similar to the workaround suggested by @michaelschmitz in the other issue (which didn’t work for me). However it is annoying to have to think of it everywhere you use the file. And still you wouldn’t expect the file to disappear immediately when there is a validation error on some other field (and the model isn’t actually updated).I’ve been seeing this same issue when an update validation fails at the same time as a new image is uploaded. I believe it is because the
has_one_attached
macro does:and this occurs before validation. (The
has_many_attached
macro is coded similarly.)Specifically, the new attachment change is rolled back in the transaction, but the
PurgeJob
still destroys the old blob.We are seeing a slightly different issue, albeit related: If a model fails validation upon submit, the image attached to it is still being uploaded. Obviously, we have no record of it now, but the bucket still gets the image. GCS and S3 are configured as mirrors; S3 is primary. The image shows up in GCS.
Hello from 2023
Its 2022 are there any workarounds
Hey Mid 2022!!
Hi @JoseMPena, I haven’t investigated further. Will keep you posted if I find something.