40

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?

Matt.
  • 1,306
  • 2
  • 13
  • 20
  • 1
    Great temporary solution, but doens't work below safari 11 if applied to all Safari browsers. Did you find any "real" solution yet? – Karem Apr 04 '18 at 13:43
  • @Karem - Thanks for that heads up! Fortunately, the users of my application won't be below Safari 11 but that's good to know. And no, no solution yet. I filed a radar/bug report with Apple and they just today asked for sys diagnostics which I will be sending. – Matt. Apr 04 '18 at 17:12
  • Had the same issue and this post helped me so much. Thank you. FWIW I believe FormDeta#delete() is only supported in Safari 11 or later. So you might have to add a version check. https://caniuse.com/#feat=xhr2 Anyway, thank you for your info. – hkurokawa Apr 05 '18 at 15:43
  • Yeah delete() may be it. Any suggestions on how to add a version check with javascript? – Karem Apr 06 '18 at 07:44
  • @Matt. thank you. Do you have a link towards the Apple report you filed ? – Laurent.B Apr 11 '18 at 18:35
  • @Laurent.B I don't since it was filed via Bug Reporter on Apple's Developer website. (Unless there's a way to share those) – Matt. Apr 11 '18 at 22:34
  • @Matt I don't know, I'm not an Apple user. I suggest (if not already done) that you send them the link to this Q/A. There are few others concerning the same problem with Safari (it's not so new as I thought it at first)... They may not be aware :/ – Laurent.B Apr 14 '18 at 16:45
  • @Laurent.B In my bug report, I did originally provide a link to this StackOverflow page. So hopefully that helps them. – Matt. Apr 14 '18 at 19:47
  • @Matt perfect. Thx – Laurent.B Apr 15 '18 at 09:22
  • 1
    There is bugzilla item open for WebKit, and also a rdar it seems. https://bugs.webkit.org/show_bug.cgi?id=184490 – Thomas Apr 16 '18 at 17:53
  • A fix has been committed to Webkit https://trac.webkit.org/changeset/230963/webkit. Will wait for next build with this in it to test. – Matt. Apr 24 '18 at 18:39
  • Yes, the issue has been resolved, I can confirm. No idea when it will be in the public release. – Matt. Apr 24 '18 at 19:07
  • This still is an issue for me in iOS 11.4. And the provided solution does not work – Erick Maynard Jun 01 '18 at 05:58
  • @ErickMaynard The fix has not made it into the version of Safari in iOS 11.4. I also doubt it'll make it in the public release of macOS 10.13.5. The provided solution does work. People can help you if you share what you've tried and/or what you have :) – Matt. Jun 01 '18 at 13:25
  • In Safari that's included in iOS 12 beta 1 has the bug fix included. I assume the same is true for macOS Mojave beta 1. – Matt. Jun 06 '18 at 13:57
  • Any idea when this will make it's way to Safari for everyday users? – drooh Aug 29 '18 at 20:57
  • @drooh Not until Safari 12, which will release when iOS 12 and macOS Mojave release, likely mid-September. – Matt. Aug 30 '18 at 15:10

6 Answers6

18

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.)

ypresto
  • 975
  • 1
  • 13
  • 23
  • 1
    Thanks @ypresto. I’ll be investigating this! Btw, iOS 11.2 uses Safari 11.0.3, which is why the issue is not present there. – Matt. Apr 14 '18 at 13:24
  • Thanks a lot! We have nginx error 400 on iphone browsers and this trick saved us! – MrAdib Sep 15 '18 at 15:00
6

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();
    }
});
mani_007
  • 628
  • 7
  • 14
  • 1
    The problem with this is that I’d have to restore the field within the always() method. Once the AJAX call is done, the user needs to interact with the form again, including the file inputs. – Matt. Apr 08 '18 at 14:25
  • I understand perhaps you should add them back in callback of Ajax method, this workaround worked for me. – mani_007 Apr 10 '18 at 06:30
  • 3
    You could just add a "disabled" attribute on the input, so it wouldn't be collected in the form data; you can then remove the attribute when you have files to upload. – Matteo Steccolini Apr 23 '18 at 22:06
  • @MatteoSteccolini solution works, I used this instead of remove. $(this).prop("disabled", true); and if you need it back just do the same with $(this).prop("disabled", false); – magorich Jul 16 '18 at 18:12
5

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.

Matt.
  • 1,306
  • 2
  • 13
  • 20
2

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]);
...
  • This is totally true, you saved my life! This is a must do for safari if you're uploading files with ajax – Jose Jet Aug 03 '18 at 11:34
1
    var fileNames = formData.getAll("filename[]");
    formData.delete("filename[]");
    jQuery.each(fileNames, function (key, fileNameObject) {
        if (fileNameObject.name) {
            formData.append("filename[]", fileNameObject);
        }
    });

Try this !!

0

This works for me to check if the input field is empty. If empty, disable the input field before created the FormData. After created the FormData, remove the "disabled" attribute. The difference to other answers is, that I search for "input[0].files.length == 0".

// get the input field with type="file"
var input = $('#myForm').find("input[type='file']")

// add the "disabled" attribute to the input
if (input[0].files.length == 0) {
  input.prop('disabled', true);
}

// create the formdata  
var formData = new FormData($(this)[0]);

// remove the "disabled" attribute
input.prop('disabled', false);
Martin
  • 83
  • 3
  • 16