Uploading multiple files at once with Ajax and XHR2

Uploading multiple files at once with Ajax and XHR2

This post is more than 2 years old.

Almost a year ago I wrote a blog post discussing how to use the Cordova FileTransfer plugin to upload multiple files (Processing multiple simultaneous uploads with Cordova). In that post I demonstrated how you could wrap the calls to each upload in a Promise and then wait for them all to complete. A reader pointed out the obvious issue with that solution - for N files you're creating N HTTP requests. While probably not a big deal (and as the developer, you could put a limit on how many files were allowed to be sent), I thought it would be interesting to demonstrate how can upload multiple files with one POST request using XHR2.

First off - a quick refresher. What is XHR2? Easy - the second version of XHR. Ok, sorry, that was a bit sarcastic. XHR is the abbreviation of XMLHttpRequest and is how JavaScript performs Ajax requests. Basically, XHR is what you think of as Ajax. If you've only ever done Ajax via jQuery, then just know that $.get is wrapping calls to an XHR object behind the scenes. Working with XHR without jQuery isn't terribly difficult - you can start with this great guide at MDN for a refresher.

XHR2 represents the more recent version of the overall spec (details here) and includes the ability to send file uploads via Ajax. Previously this was done with products like Adobe Flash. Support for XHR2 is rather nice:

CanIUse.com data

And of course, if you want to use this in Cordova, you are definitely safe to do so, especially if you use Crosswalk on the Android side.

So, how do you send a file using XHR2? For that I'd suggest reading the excellent MDN article, Using FormData Objects, which walks you through the process of creating a FormData object in JavaScript and assigning a file to it. I won't repreat the documentation there as it is short and sweet (and once again I will remind folks that the Mozilla Developer Network is one of the best damn resources on the Internet for web developers) but rather focus on how we could use it to send multiple files at once.

Let's begin with an incredibly simple handler for our uploads. The following Node application defines a route, /upload, that uses the Formidable package to process file uploads. It literally just dumps them to console so it really isn't "handling" it, but it gave me an easy way to see my form POSTs come in and ensure they were being sent the right way.


var express = require('express');
var app = express();
var formidable = require('formidable');

app.set('port', process.env.PORT || 3000);

app.use(function(req, res, next) {
	res.header("Access-Control-Allow-Origin", "*");
	res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
	next();
});

app.post('/upload', function(req, res) {
	var form = new formidable.IncomingForm();
	form.parse(req, function(err, fields, files) {

		console.log('handling form upload - fields', fields);
		
		console.log('handling form upload - files', files);
		
	});
	res.send('Thank you');
});

app.listen(app.get('port'), function() {
	console.log('Express running on http://localhost:' + app.get('port'));
});

Ok, now let's look at the front end. For my first demo, I created the following HTML.


<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<meta name="description" content="">
		<meta name="viewport" content="width=device-width">
	</head>
	<body>

	<form id="testForm">
		<input type="file" id="file1"><br/>
		<input type="file" id="file2"><br/>		
		<input type="file" id="file3"><br/>
		<input type="submit">
	</form>
		 
	<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
	<script src="app.js"></script>
	</body>
</html>

As you can see - I've got 3 form fields - all of which are file types. Now let's look at the code.


var $f1, $f2, $f3;

$(document).ready(function() {
	$('#testForm').on('submit', processForm);
	$f1 = $('#file1');
	$f2 = $('#file2');
	$f3 = $('#file3');
	
});

function processForm(e) {
	e.preventDefault();
	console.log('processForm');
	
	var formData = new FormData();

	if($f1.val()) formData.append('file1', $f1.get(0).files[0]);
	if($f2.val()) formData.append('file2', $f2.get(0).files[0]);
	if($f3.val()) formData.append('file3', $f3.get(0).files[0]);


	var request = new XMLHttpRequest();
	request.open('POST', 'http://localhost:3000/upload');
	request.send(formData);
	
	request.onload = function(e) {
		console.log('Request Status', request.status);
	};
	
}

So first off - yes I'm using jQuery and naked XHR calls. That's ok. You don't have to use jQuery and that's fine too. Don't stress over it. I begin by adding a submit handler and a quick reference to my three fields. When you submit the form, I create a new FormData object. The MDN article I linked to earlier talks about it more, but you can think of it like a virtual form. I can add simple name/value pairs (like name = 'Raymond') as well as files. I check the value of each field to determine if something was selected. Here's an important point though. The value will be a string. In Chrome it looks something like C:\fakepath\aliens.jpg. Yes, I'm on a Mac and yes, that's a fake Windows path. The idea here is to obscure the real path so that nasty code can't be used as a vector to attack your system.

The FormData object though wants a real file. We use $f1.get(0) to connect to the real DOM item (not the jQuery-wrapped object) and then use the files property to gain read access to the selected file.

That's repeated for each of the three fields and then I simply post to my server.

And yeah - that's it. When I test, I can see my files come in as expected.

Console dump

So far so good. But don't forget that some HTML form tags support a multiple attribute. In v2 of my sample, I changed the three form fields to this:


&;lt;input type="file" id="file1" multiple>

I can now select 0-N files when submitting my form. Here is the modified version of the code to handle that.


var $f1;

$(document).ready(function() {
	$('#testForm').on('submit', processForm);
	$f1 = $('#file1');	
});

function processForm(e) {
	e.preventDefault();
	console.log('processForm');
	
	var formData = new FormData();
	if($f1.val()) {
		var fileList = $f1.get(0).files;
		for(var x=0;x<fileList.length;x++) {
			formData.append('file'+x, fileList.item(x));	
			console.log('appended a file');
		}
	}

	var request = new XMLHttpRequest();
	request.open('POST', 'http://localhost:3000/upload');
	request.send(formData);
	
	request.onload = function(e) {
		console.log('Request Status', request.status);
	};
	
}

I grab that files property and treat it like a simple array. It isn't quite an array though (MDN docs for FileList) as I have to use a item method to fetch the particular value. Then it's simply a matter of appending to formData with a unique name for each file. This is treated the exact same by the back-end so it just works.

All in all - pretty simple I think and could be used easily in a Cordova application. You can also attach a progress event handler and monitor the process of sending up the binary data. Enjoy and let me know if you have any questions by leaving a comment below.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Akash Pal posted on 5/5/2016 at 8:46 PM

Is there anyway to get the file object without using <input type="file"> ?
Is it possible to create a file object using filePath?

Comment 2 (In reply to #1) by Raymond Camden posted on 5/5/2016 at 8:50 PM

In a web app, no, that would be a security issue. In a Cordova app... maybe. If the app itself has access to the file.

Comment 3 (In reply to #2) by Akash Pal posted on 5/5/2016 at 8:52 PM

This wont work on android as <input type="file"> is not supported in android. This would work perfectly on ios.
For this to work Cordova has to convert the file path to file object.

Comment 4 by Akash Pal posted on 5/5/2016 at 8:56 PM

Kindly do update , if there is way to make this work in android cordova web app.

Comment 5 (In reply to #2) by Akash Pal posted on 5/5/2016 at 9:19 PM

Thank you for writing this blog post. Hope some solution works out for android.

Comment 6 (In reply to #5) by Raymond Camden posted on 5/5/2016 at 9:22 PM

You can pass a Blob to the FormData append method (https://developer.mozilla.o.... So in theory, you could read in a blob from the file system. Let me see if I can come up with a new demo that doesn't use file pickers and is Cordova ready.

Comment 7 (In reply to #6) by Akash Pal posted on 5/5/2016 at 9:26 PM

Yes i came across the Bolb thing too but that too didn't work well.
I have already raised an issue to apache for improvement for the file transfer plugin to enable multiple uploads in single request.
However until then some work around is required; especially for android. As in ios even without using file transfer plugin uploading using <input type="file" multiple=""> works.

Comment 8 (In reply to #7) by Raymond Camden posted on 5/5/2016 at 9:32 PM

How did it fail with the blob?

Comment 9 (In reply to #8) by Akash Pal posted on 5/5/2016 at 9:35 PM

I had tried to create blob object but it didn't work, maybe i wasn't doing it right.I shall have to checkout again.

Comment 10 (In reply to #9) by Raymond Camden posted on 5/5/2016 at 9:41 PM

I'll give it a shot too.

Comment 11 (In reply to #10) by Akash Pal posted on 5/5/2016 at 9:43 PM

okay.I have been working on this issue for almost a week. And there are no materials or guidance anywhere to follow up.

Comment 12 (In reply to #4) by Raymond Camden posted on 5/6/2016 at 2:00 PM

I've been able to get this working - hoping to post an update later today.

Comment 13 (In reply to #12) by Raymond Camden posted on 5/6/2016 at 3:14 PM

Posted.

Comment 14 by WebReflection posted on 5/13/2016 at 3:52 PM

Raymond I liked the intent but the execution was pretty poor. It would've taken so little to make it a great post. I've fixed all bad practices mentioned here in this gist: https://gist.github.com/Web... please consider some update, thanks.

Eidt: I've updated the gist hoping its content will be considered as update. I also would appreciate if the moderator could remove insults from comments, like the one following.

Thanks.

Comment 15 (In reply to #14) by Scott Stroz posted on 5/13/2016 at 7:33 PM

Wow.....kind of a dick-ish mover there.

Comment 16 (In reply to #15) by WebReflection posted on 5/13/2016 at 8:39 PM

I'm sorry you think that. I've been writing articles for 10+ years and I do care about best practices. The usage of jQuery is OK but defining global variables, write non standard forms to upload, avoid reasonable standard practices at all cost to use $ instead of querySelector is IMO not acceptable in 2016 and from IBM's Engineer. We, posts authors, should've learned by today the side effect these articles have in the long term and writing careless justifying it with a couple of "it's OK, don't stress it" looks and feel bad. I'm sure the author of this post has interest in being considered someone that suggests the right thing, and I'm also sure he's more mature than you and will consider some change/update instead of just insult me. I care about what the community learn, so should authors with a respectable level, experience, and knowledge.

Comment 17 (In reply to #16) by Scott Stroz posted on 5/13/2016 at 8:47 PM

I was not referring to the fact that you wanted to point out 'best practices', rather, the manner in which you chose to do it - not just yoru comment, but the comments in the link you provided.

You need to work on your people skills (and this is coming from someone who just called you a dick).

Comment 18 (In reply to #17) by WebReflection posted on 5/13/2016 at 9:06 PM

The link is a gist that's simpky waiting to be removed. I'm a bit fed-up of rushed articles and I was in a rush when I've written the gist. With the will of posting, should come responsibilities, and this was, and still is, my point. To write better code takes less than write justifications about poor code. Last, but not least, I'm not an expert in this field but I believe you shouldn't justify your free insults pointing fingers around. However I'm not willing to discuss this. Best Regards.

Comment 19 (In reply to #17) by WebReflection posted on 5/13/2016 at 9:30 PM

P.S. your website is down with a cold fusion error. In case you didn't know. Best Regards

Comment 20 (In reply to #14) by Brett Layman posted on 5/15/2016 at 11:05 PM

I can see your point about global variables, but that's an easy fix and doesn't require converting all of the jquery to pure javascript. If using jquery falls within best practices, then why not keep the jquery and only change what you have to? When correcting someone's mistakes, I think it's best to stick with their approach as much as possible, and only change what is truly a bad practice (rather than a personal preference). I am curious though, what is the downside of using .get(0)? Does it create security issues?

Comment 21 (In reply to #20) by WebReflection posted on 5/16/2016 at 4:14 AM

Using `$('css').get(0)` instead of `documenti.querySelector('css')` is kinda silly. You require 100KB library and all the magic it has and you ignore it entirely. There's no reason on earth to consider this a good practice. It's lazynesd and bad habit at its best. There's no reason at all to suggest jQuery in the entire snippet. Even using ready to reach the form through a script defined after is kinda pointless. As summary, suggesting a dependency when it's easily avoidable and not actually used for its strength or its magic at all is a bad practice, hence the fix. You can live using always jQuery, it's unnecessary but OK. Although I've shown better, modern, approach. We're using an API that is available in browsers where jQuery doesn't fix much in terms of selectors.

Comment 22 (In reply to #21) by Brett Layman posted on 5/16/2016 at 4:43 PM

Yeah I guess that using .get() is a pretty roundabout way of selecting a dom element. But in other cases I find that jQuery is more succinct and readable, even if it is unnecessary. I just thought your critique could be more minimalist in terms of your corrections, like just choosing a couple of things and going into more depth about why those practices are problematic.

Comment 23 (In reply to #22) by WebReflection posted on 5/16/2016 at 4:45 PM

$('css').get(0) is like climbing a wall instead of using its main door: document,querySelector('css'). This is a bad practice, this post and its content didn't need jQuery, hence the fix.