Download file via ajax

Hello and welcome 😀,

In a previous post we discussed how to create a zip file on demand on the server so that it can be served via Web API. The code for the previous post and this one for can be found in my github repository.

So today we’re going to look at how to retrieve the file from a web page that uses javascript for it’s interactions (in my case it that inspired this was a React.js application).

First we’re going to cover why do we need this workaround and secondly what solution I ended up using by doing some research on StackOverflow, as well as MDN to figure out how and why it works.

To test this out we’re going to create an empty HMTL page with a button on it. Next we’re going to hook that button up with an event for when it’s clicked. The HTML will look something like this:

<button type="button" id="testButton" onclick="getArchiveAjax()">Get Archives</button>

Next up I added a script tag for Axios since I find it very easy to use especially for this scenario, though since the issue is browser related, you could use any framework/library you wish that allows you to implement this workaround.

So to add Axios without too much fuss for this case, I added the following script tag in the head of the page:

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

So now that the infrastructure is in place, we ca go on and start implementing the getArchiveAjax function. We will start simple just so that we can also learn what the issue is.

function getArchiveAjax(){
    axios.get("/ServerSideZip/getArchive").then(x=> {
        console.log(x)
    });
}

When we run this function by clicking the button we will see that the response that comes back, contains the bytes of the file in Base64 format. In theory, we could write it to disk like that, but javascript in the browser doesn’t let us do that due to security concerns because we could end up with all sorts of malware from websites.

Because of this security concerns, browsers only let you download files from a user interaction, or a simulated used interaction.

For this, we need to create a link on the page so that either the user can click it, or, in our case, the script can click the link. So we’re going to look into how to create a URL out of a byte array by using the static URL functionality form javascript.

As we can see from the documention, the URL.createObjectURL() function requires as an input a blob. Luckily for us we can instruct Axios to request the file as a blob using the follosing configuration:

function getArchiveAjax(){
    axios.get("/ServerSideZip/getArchive",{
        responseType: "blob"
    }).then(x=> {
        console.log(x);
    });
}

With this now we will receive the data as a Blob and as such we could create the URL from it using the URL.createObjectURL() function which stores the file in memory and provides an unique URL towards that file.

function getArchiveAjax(){
    axios.get("/ServerSideZip/getArchive",{
        responseType: "blob"
    }).then(x=> {
        const url = URL.createObjectURL(x.data); 
        console.log(url);
        URL.revokeObjectURL(url);
    });
}

From the documentation we also learned that it is a good practice to revoke the URL so that we release the used up memory.

Next up, we need to create the hyperlink for the file so that it can be clicked, first, we create the anchor element dynamically, provide it the URL attach it to the DOM, click it, and then we remove the element so that we clean up after ourselves. The code looks as follows:

function getArchiveAjax(){
    axios.get("/ServerSideZip/getArchive",{
        responseType: "blob"
    }).then(x=> {
        const url = URL.createObjectURL(x.data); 
        const link = document.createElement('a');
        link.href = url;
        document.body.append(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(url);
    });
}

When we run this we will see that the save file dialog shows up (I’m using Firefox), but something is odd about the file name.

This is easy to file by adding and additional attribute to the generated hyperlink:

function getArchiveAjax(){
    axios.get("/ServerSideZip/getArchive",{
        responseType: "blob"
    }).then(x=> {
        const url = URL.createObjectURL(x.data); 
        const link = document.createElement('a');
        link.href = url;
        link.setAttribute('download', "TestFileNameClient.zip");
        document.body.append(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(url);
    });
}

Now when we get the file dialog we will see the it with the name we gave it:

But we can do one better, because the server might actually send file name associated with that zip file, which we can extract from the response header with a little string manipulation which gets us to our final form of the getArchiveAjax function:

function getArchiveAjax(){
    axios.get("/ServerSideZip/getArchive",{
        responseType: "blob"
    }).then(x=> {
        const filenameHeader = x.headers["content-disposition"].split(';')[1];
        const filename = filenameHeader.substring(filenameHeader.indexOf("=")+1);
        const url = URL.createObjectURL(x.data); 
        const link = document.createElement('a');
        link.href = url;
        link.setAttribute('download', filename);
        document.body.append(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(url);
    });
}

I this is the way I ended up using the functionality for downloading a file via a Web API call and saving it on disk.

I hope you enjoyed the post and that it has helped you.

Thank you and see you next time,

Vlad V.

Leave a comment