php - ajax file upload fails only with SSL terminating load balancer

143

A file upload with ajax works fine when we don't use SSL, but fails when we do. That is, on our development server (no SSL), the following works. It also worked fine on production before we put in SSL.

Our server infrastructure (at rackspace) now has SSL termination on the load balancer so that sessions can be pegged to one of the web servers. We also use .htaccess to check {HTTP:X-Forwarded-Proto} and force use of HTTPS. If we remove the .htaccess that forces SSL and make the following requests with HTTP, it works. Putting the .htaccess back in place, everything else still works fine except for the file upload portion. All other ajax calls are fine, forms get built and buttons work...

Here's the .htaccess file:

RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^.*$ https://%{HTTP_HOST}%{REQUEST_URI}

js/widget-groups.js builds a modal form to create or update details on a group, including a file upload for the group logo. It's based on UserFrosting, and the form isn't especially interesting. The relevant bits are:

    /* Display a modal form for updating/creating a group */
function groupForm(box_id, group_id) {
    group_id = typeof group_id !== 'undefined' ? group_id : "";

    // Delete any existing instance of the form with the same name
    if($('#' + box_id).length ) {
        $('#' + box_id).remove();
    }

    var data = {
        box_id: box_id,
        render_mode: 'modal'
    };

    if (group_id != "") {
        console.log("Update mode");
        data['group_id'] = group_id;
        data['show_dates'] = true;
    }

    // Generate the form
    $.ajax({
      type: "GET",
      url: "load_form_group.php",
      data: data,
      dataType: 'json',
      cache: false
    })
    .fail(function(result) {
        addAlert("danger", "Oops, looks like our server might have goofed.  If you're an admin, please check the PHP error logs.");
        alertWidget('display-alerts');
    })
    .done(function(result) {
        // Append the form as a modal dialog to the body
        $( "body" ).append(result['data']);
        $('#' + box_id).modal('show');

        // Link submission buttons
        $('#' + box_id + ' form').submit(function(e){
            var errorMessages = validateFormFields(box_id);
console.log("in widget Link submission buttons");
            var myFileList = document.getElementById('image_url').files;
            myFileList = typeof myFileList !== 'undefined' ? myFileList : false;
console.log(myFileList);
/*
            var myFile = myFileList[0];
console.log(myFile);
console.log("filename="+myFile.name+",size="+myFile.size+",type="+myFile.type);
*/

            if (errorMessages.length > 0) {
                $('#' + box_id + ' .dialog-alert').html("");
                $.each(errorMessages, function (idx, msg) {
                    $('#' + box_id + ' .dialog-alert').append("<div class='alert alert-danger'>" + msg + "</div>");
                });
            } else {
                if (group_id != "") {
                    updateGroup(myFileList,box_id, group_id);
                    } else {
                    createGroup(myFileList,box_id);
                    }
            }

            e.preventDefault();
        });
    });
    }

When the user hits the button, the call is made to updateGroup:

// Update group with specified data from the dialog
function updateGroup(myFileList,dialog_id, group_id) {
var data = {
    group_id: group_id,
    display_name: $('#' + dialog_id + ' input[name="display_name"]' ).val(),
    group_logo: $('#' + dialog_id + ' input[name="group_logo"]' ).val(),
    csrf_token: $('#' + dialog_id + ' input[name="csrf_token"]' ).val(),
    compAddr1: $('#' + dialog_id + ' input[name="compAddr1"]' ).val(),
    compAddr2: $('#' + dialog_id + ' input[name="compAddr2"]' ).val(),
    compAddr3: $('#' + dialog_id + ' input[name="compAddr3"]' ).val(),
    compCity: $('#' + dialog_id + ' input[name="compCity"]' ).val(),
    compSt: $('#' + dialog_id + ' input[name="compSt"]' ).val(),
    compZip: $('#' + dialog_id + ' input[name="compZip"]' ).val(),
    compCountry: $('#' + dialog_id + ' input[name="compCountry"]' ).val(),
    ajaxMode:   "true"
}

var url = "update_group.php";
$.ajax({
  type: "POST",
  url: url,
  data: data,
}).done(function(result) {
    processJSONResult(result);
    saveImageUrl(myFileList,group_id);
    window.location.reload();
});
return;
}

which succeeds and in turn calls saveImageUrl:

    function saveImageUrl(myFileList,group) {
    if (group==false) return false;
    if (myFileList.length!=1) return false;
        // Create a temporary formdata object and add the files
            myurl="upload.php";
console.log("saveImageUrl. myFileList="+myFileList+". group=" + group + ". myurl=" + myurl);
console.log("myFileList[length]="+myFileList[length]);
console.log("myFileList.length="+myFileList.length);
            var data = new FormData();
            data.append("group",group);
            $.each(myFileList, function(key, value)
                {
                data.append(key, value);
                });
/** alternate
            var xhr = new XMLHttpRequest();
            xhr.open('POST','upload.php',true);
            xhr.onreadystatechange = function() {
                console.log("onreadystatechange state="+xhr.readyState);
                console.log("onreadystatechange status="+xhr.status);
                console.log("onreadystatechange responseText="+xhr.responseText);
                };
            xhr.send(data);
**/
            $.ajax({
                url: myurl,
                type: 'POST',
                data: data,
                cache: false,
                processData: false, // Don't process the files
                contentType: false, // Set content type to false as jQuery will tell the server its a query string request
                success: function(data)
                    {
console.log("upload success with data=");
console.log(data);
                    },
                error: function(data)
                    {
console.log("upload error with data=");
console.log(data);
                    }
                });
console.log("image_url=" + image_url);
}

All the extra comments were an attempt to see what was going on.

The upload.php file starts and ends with:

<?php
file_put_contents('uploads/debug.txt','---upload+',FILE_APPEND);

...

echo json_encode(array(
    "files" => $files,
    "errors" => count($errors),
    "successes" => count($successes)));

?>

Again, the evidence shows that the file_put_contents works fine when we're not using SSL but doesn't get touched when we are. Using firebug, the console shows the call to upload.php being made, though it always shows it in red and we end up in the error: block, but the there is no content to the "console.log(data)" line. That may just be an issue with the return json, though that looks ok to me. The real issue, I think, is that it succeeds when not using SSL but fails when we do put it in place. Success being, the file is uploaded and saved in the right place by update.php.

I don't understand how the SSL change impacts things. Other ajax calls are fine; it's only a problem with the one that has

processData: false, // Don't process the files
contentType: false, // Set content type to false or jQuery will tell the server its a string

which I understand to be required in order to properly handle ajax file uploads.

Is there more happening under the covers that I don't understand?

Thanks, Eric

938

Answer

Solution:

The core of the issue is the sequential ajax calls. In development and in non-SSL production, it works out fine. With SSL, though, the real issue surfaces: timing.

The solution was to add:

async: false,

to the ajax call to upload.php. Adding it to the first ajax request didn't help. Only adding it to the second.

Like many questions, it wasn't quite accurate in the original post, but I found plenty of similar questions with not many satisfactory answers. Hopefully this will be one more clue in someone else's search.

Eric

People are also looking for solutions to the problem: php - Wordpress Plugin Undefined index: HTTP_REFERER

Source

Didn't find the answer?

Our community is visited by hundreds of web development professionals every day. Ask your question and get a quick answer for free.

Ask a Question

Write quick answer

Do you know the answer to this question? Write a quick response to it. With your help, we will make our community stronger.

Similar questions

Find the answer in similar questions on our website.