Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Safari 11.1: ajax/XHR form submission fails when input[type=file] is empty

UPDATE: As of Webkit build r230963, this issue has been resolved in Webkit.

===========

Since the recent Safari 11.1 update on macOS and iOS, as well as in Safari Technology Preview 11.2, the $.ajax calls in my web application are failing when a input[type=file] field has no file chosen (it isn't required in my form). No failure when the field does have a file chosen.

The error callback of ajax runs and the Safari console contains the following message: Failed to load resource: The operation couldn’t be completed. Protocol error. I am HTTPS and submitting to a location on the same domain (and server) also over HTTPS.

Before the 11.1 update, the $.ajax call submitted just fine when no file was chosen. The latest versions of Chrome and Firefox have no issues.

Relevant parts of my code:

The input:

Browse... <input id="file-upload" type="file" name="image" accept=".jpg,.jpeg">

The JS:

var formData = new FormData($(this)[0]);
$.ajax({
    type: 'POST',
    enctype: 'multipart/form-data',
    url: '../process.php',
    data: formData,
    contentType: false,
    processData: false,
    cache: false,
    success: function(response) { ... },
    error: function() { //my code reaches here }
});

As a temporary (hopefully) solution, I'm detecting an empty file field and removing it from formData before the ajax call and everything works as expected/before:

$("input[type=file]").each(function() {
    if($(this).val() === "") {
        formData.delete($(this).attr("name"));
    }
});

Am I doing something wrong, is there an issue with Safari, or is there a change in Safari that needs to be accounted for now in ajax calls?

like image 710
Matt. Avatar asked Apr 02 '18 15:04

Matt.


4 Answers

UPDATE: Old answer does NOT work in Firefox.

Firefox returns just empty string for FormData.get() on empty file field (instead of File object in other browsers). So when using old workaround, empty <input type="file"> will be sent like as empty <input type="text">. Unfortunately, there is no way to distinguish an empty file from an empty text after creating FormData object.

Use this solution instead:

var $form = $('form')
var $inputs = $('input[type="file"]:not([disabled])', $form)
$inputs.each(function(_, input) {
  if (input.files.length > 0) return
  $(input).prop('disabled', true)
})
var formData = new FormData($form[0])
$inputs.prop('disabled', false)

Live Demo: https://jsfiddle.net/ypresto/05Lc45eL/

For non-jQuery environment:

var form = document.querySelector('form')
var inputs = form.querySelectorAll('input[type="file"]:not([disabled])')
inputs.forEach(function(input) {
  if (input.files.length > 0) return
  input.setAttribute('disabled', '')
})
var formData = new FormData(form)
inputs.forEach(function(input) {
  input.removeAttribute('disabled')
})

For Rails (rails-ujs/jQuery-ujs): https://gist.github.com/ypresto/cabce63b1f4ab57247e1f836668a00a5


Old Answer:

Filtering FormData (in Ravichandra Adiga's answer) is better because it does not manipulate any DOM.

But the order of parts in FormData is guaranteed to be the same order to input elements in form, according to <form> specification. It could cause another bug if someone relies on this spec.

Below snippet will keep FormData order and empty part.

var formDataFilter = function(formData) {
    // Replace empty File with empty Blob.
  if (!(formData instanceof window.FormData)) return
  if (!formData.keys) return // unsupported browser
  var newFormData = new window.FormData()
  Array.from(formData.entries()).forEach(function(entry) {
    var value = entry[1]
    if (value instanceof window.File && value.name === '' && value.size === 0) {
      newFormData.append(entry[0], new window.Blob(), '')
    } else {
      newFormData.append(entry[0], value)
    }
  })
  return newFormData
}

Live example is here: https://jsfiddle.net/ypresto/y6v333bq/

For Rails, see here: https://github.com/rails/rails/issues/32440#issuecomment-381185380

(NOTE: iOS 11.3's Safari has this issue but 11.2 is not.)

like image 102
ypresto Avatar answered Nov 20 '22 10:11

ypresto


For workaround I delete the input type file completely from DOM using jQuery remove() method.

$("input[type=file]").each(function() {
    if($(this).val() === "") {
        $(this).remove();
    }
});
like image 20
mani_007 Avatar answered Nov 20 '22 12:11

mani_007


As of Webkit build r230963, this issue has been resolved in Webkit. I downloaded and ran that build and confirmed the issue is resolved. Not idea when a public release will be available for Safari that contains this fix.

like image 5
Matt. Avatar answered Nov 20 '22 11:11

Matt.


I worked on what appears to be the same issue in a Perl-program

Processing multipart/form-data in Perl invokes Apache-error with Apple-devices, when a form-file-element is empty

Workaround is removing the form-elements before the formdata is assigned:

$('#myForm').find("input[type='file']").each(function(){
    if ($(this).get(0).files.length === 0) {$(this).remove();}
});
var fData = new FormData($('#myForm')[0]);
...
like image 2
Roger Van Montfort Avatar answered Nov 20 '22 10:11

Roger Van Montfort