Alex Popov
Alex Popov

Reputation: 75

How to send binary file to the server using JavaScript

I'm working on file encryption for my messenger and I'm struggling with uploading the file after encryption is done.

The encryption seems fine in terms of performance, but when I try to make an upload, the browser hangs completely. Profiler writes "small GC" events infinitely, and the yellow bar about the hung up script is appearing every 10 seconds.

What I already tried:

  1. Read the file with FileReader to ArrayBuffer, then turn it into a basic Array, encrypt it, then create a FormData object, create a File from the data, append it to FormData and send. It worked fast with original, untouched file around 1.3 Mb in size when I did not do the encryption, but on the encrypted "fake" File object after upload I get file with 4.7 Mb and it was not usable.

  2. Send as a plain POST field (multipart formdata encoding). The data is corrupted after it is saved on PHP this way.

  3. Send as a Base64-encoded POST field. Finally it started working this way after I found a fast converting function from binary array to Base64 string. btoa() gave wrong results after encode/decode. But after I tried a file of 8.5 Mb in size, it hung again.

  4. I tried moving extra data to URL string and send file as Blob as described here. No success, browser still hangs.

  5. I tried passing to Blob constructor a basic Array, a Uint8Array made of it, and finally I tried to send File as suggested in docs, but still the same result, even with small file.

What is wrong with the code? HDD load is 0% when this hang happens. Also the files in question are really very small

Here is what I get on the output from my server script when I emergency terminate the JS script by pressing the button:

Warning: Unknown: POST Content-Length of 22146226 bytes exceeds the limit of 8388608 bytes in Unknown on line 0

Warning: Cannot modify header information - headers already sent in Unknown on line 0

Warning: session_start() [function.session-start]: Cannot send session cache limiter - headers already sent in D:\xmessenger\upload.php on line 2
Array ( ) 

Here is my JavaScript:

function uploadEncryptedFile(nonce) {
    if (typeof window['FormData'] !== 'function' || typeof window['File'] !== 'function') return
    
    var file_input = document.getElementById('attachment')
    
    if (!file_input.files.length) return
    var file = file_input.files[0]
    
    var reader = new FileReader();
    
    reader.addEventListener('load', function() {
        var data = Array.from(new Uint8Array(reader.result))
        var encrypted = encryptFile(data, nonce)
        //return  //Here it never hangs
        var form_data = new FormData()
        form_data.append('name', file.name)
        form_data.append('type', file.type)
        form_data.append('attachment', arrayBufferToBase64(encrypted))
        /* form_data.append('attachment', btoa(encrypted)) // Does not help */
        form_data.append('nonce', nonce)
        var req = getXmlHttp()
        req.open('POST', 'upload.php?attachencryptedfile', true)
        req.onload = function() {
            var data = req.responseText.split(':')
            document.getElementById('filelist').lastChild.realName = data[2]
            document.getElementById('progress2').style.display = 'none'
            document.getElementById('attachment').onclick = null
            encryptFilename(data[0], data[1], data[2])
        }
        req.send(form_data)
        /* These lines also fail when the file is larger */
        /* req.send(new Blob(encrypted)) */
        /* req.send(new Blob(new Uint8Array(encrypted))) */
    })
    reader.readAsArrayBuffer(file)
}

function arrayBufferToBase64(buffer) {
    var binary = '';
    var bytes = new Uint8Array(buffer);
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return window.btoa(binary);
}

Here is my PHP handler code:

if (isset($_GET['attachencryptedfile'])) {
    $entityBody = file_get_contents('php://input');
    if ($entityBody == '') exit(print_r($_POST, true)); 
    else exit($entityBody);
    
    if (!isset($_POST["name"])) exit("Error");
    
    $name = @preg_replace("/[^0-9A-Za-z._-]/", "", $_POST["name"]);
    $nonce = @preg_replace("/[^0-9A-Za-z+\\/]/", "", $_POST["nonce"]);
    
    if ($name == ".htaccess") exit();

    $data = base64_decode($_POST["attachment"]);
    //print_r($_POST);
    //exit();
    if (strlen($data)>1024*15*1024) exit('<script type="text/javascript">parent.showInfo("Файл слишком большой"); parent.document.getElementById(\'filelist\').removeChild(parent.document.getElementById(\'filelist\').lastChild); parent.document.getElementById(\'progress2\').style.display = \'none\'; parent.document.getElementById(\'attachment\').onclick = null</script>');
    $uname = uniqid()."_".str_pad($_SESSION['xm_user_id'], 6, "0", STR_PAD_LEFT).substr($name, strrpos($name, "."));
    file_put_contents("upload/".$uname, $data);
    mysql_query("ALTER TABLE `attachments` AUTO_INCREMENT=0");
    mysql_query("INSERT INTO `attachments` VALUES('0', '".$uname."', '".$name."', '0', '".$nonce."')");
    exit(mysql_insert_id().":".$uname.":".$name);
}

HTML form:

<form name="fileForm" id="fileForm" method="post" enctype="multipart/form-data" action="upload.php?attachfile" target="ifr">
    <div id="fileButton" title="Прикрепить файл" onclick="document.getElementById('attachment').click()"></div>
    <input type="file" name="attachment" id="attachment" title="Прикрепить файл" onchange="addFile()" />
</form>

Upvotes: 1

Views: 2615

Answers (1)

Alex Popov
Alex Popov

Reputation: 75

UPD: the issue is not solved, unfortunately. My answer is only partially correct. Now I made a silly mistake in the code (forgot to update the server side), and I found another cause of possible hang. If I submit a basic POST form (x-www-urlencoded) and code in the PHP script tries to execute this line ($uname is defined, $_FILES is an empty array)

if (!copy($_FILES['attachment']['tmp_name'], "upload/".$uname)) exit("Error");

then the whole thing hangs again. If I terminate the script, the server response is code 200, and the body contents are just fine (I have error output on on my dev machine). I know it is a bad thing - calling copy with the first argument which is undefined at all, but even server error 500 must not hang the browser in such a way (btw, new latest version of Firefox is also affected).

I have Apache 2.4 on Windows 7 x64 and PHP 5.3. Can someone please verify this thing? Maybe a bug should be filed to Apache/Firefox team?


Oh my God. This terrible behavior was caused by... post_max_size = 8M set in php.ini. And files smaller than 8 Mb actually did not hang the browser, as I figured it out.

Last question is - why? Why cannot Apache/PHP (I have Apache 2.4 btw, it is not old) somehow gracefully abort the connection, telling the browser that the limit is exceeded? Or maybe it is a bug in XHR implementation, and is not applicable to basic form submit. Anyway, will be useful for people who stumble upon it.

By the way, I tried it in Chrome with the same POST size limit, and it does not hang there completely like in Firefox (the request is still in some hung up condition with "no response available", but the JS engine and the UI are not blocked ).

Upvotes: 1

Related Questions