Tuesday, May 3, 2016

AngularJS : Download Files by Sending a HTTP Request to Web API

As you might already know you can’t directly use AngularJS/JQuery/JavaScript to download a file by sending a HTTP request. That’s because usually the file downloads are triggered when a user click on a link and link’s href is targeted to the file location.

But imagine that your server expects a HTTP request (of course an authenticated request), and he will be serving you the file as a octet-stream or as an Attachment in the response.

To understand the scenario properly, consider the following Web API action.
[HttpGet]
[Route("values/download")]
public HttpResponseMessage Download(string name)
{
    try
    {
        string fileName = string.Empty;
        if (name.Equals("pdf", StringComparison.InvariantCultureIgnoreCase))
        {
            fileName = "SamplePdf.pdf";
        }
        else if (name.Equals("zip", StringComparison.InvariantCultureIgnoreCase))
        {
            fileName = "SampleZip.zip";
        }
 
        if (!string.IsNullOrEmpty(fileName))
        {
            string filePath = HttpContext.Current.Server.MapPath("~/App_Data/") + fileName;
 
            using (MemoryStream ms = new MemoryStream())
            {
                using (FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                {
                    byte[] bytes = new byte[file.Length];
                    file.Read(bytes, 0, (int)file.Length);
                    ms.Write(bytes, 0, (int)file.Length);
 
                    HttpResponseMessage httpResponseMessage = new HttpResponseMessage();
                    httpResponseMessage.Content = new ByteArrayContent(bytes.ToArray());
                    httpResponseMessage.Content.Headers.Add("x-filename", fileName);
                    httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
                    httpResponseMessage.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
                    httpResponseMessage.Content.Headers.ContentDisposition.FileName = fileName;
                    httpResponseMessage.StatusCode = HttpStatusCode.OK;
                    return httpResponseMessage;
                }
            }
        }
        return this.Request.CreateResponse(HttpStatusCode.NotFound, "File not found.");
    }
    catch (Exception ex)
    {
        return this.Request.CreateResponse(HttpStatusCode.InternalServerError, ex);
    }
}
The action is expecting a URL parameter and based on the parameter it will be serving you either a pdf file or a zip file. Now to download a file by calling this endpoint, I believe what the most people would think is, following would work (including me).
$scope.downloadFile = function (name) {
   $http({
       method: 'GET',
       url: 'api/values/download',
       params: { name: name },
   }).success(function (data, status, headers) {
      
   }).error(function (data) {
       
   });
};
Now if you call the downloadFile method to send the HTTP request from client side (for instance on the ng-click event of a anchor tag), it will not download the file. The reason is because JavaScript can’t access file system to save the files.

So here is a workaround (in Stack Overflow by answered by Scott).
$scope.downloadFile = function (name) {
    $http({
        method: 'GET',
        url: 'api/values/download',
        params: { name: name },
        responseType: 'arraybuffer'
    }).success(function (data, status, headers) {
        headers = headers();
 
        var filename = headers['x-filename'];
        var contentType = headers['content-type'];
 
        var linkElement = document.createElement('a');
        try {
            var blob = new Blob([data], { type: contentType });
            var url = window.URL.createObjectURL(blob);
 
            linkElement.setAttribute('href', url);
            linkElement.setAttribute("download", filename);
 
            var clickEvent = new MouseEvent("click", {
                "view": window,
                "bubbles": true,
                "cancelable": false
            });
            linkElement.dispatchEvent(clickEvent);
        } catch (ex) {
            console.log(ex);
        }
    }).error(function (data) {
        console.log(data);
    });
};
So this is the output.
image
Download
image
Download
I have uploaded the sample to my OneDrive, so you can download and play around.


Happy Coding.

Regards,
Jaliya

24 comments:

  1. Hi, you have an example with angular 2 and Typescript?

    ReplyDelete
    Replies
    1. Hi Jose,

      I am sorry, I don't at this moment. But hopefully I will be writing a post on Angular2 and TypeScript soon!

      Jaliya

      Delete
  2. thanks you so much it has saved my life

    ReplyDelete
  3. thanks you so much it has saved my life

    ReplyDelete
  4. I tried it but it doesn't work on Safari for me..The console throws error "Failed to load resource: Frame load interrupted" and it doesn't download at all...

    ReplyDelete
  5. How to make it works with excel file?

    ReplyDelete
    Replies
    1. Hey man try this, it just worked for me:

      var blob = new Blob([data], {type : contentType + ';charset=UTF-8'});
      linkElement.setAttribute("download", 'filename.xls');

      Delete
  6. In IOS this is not working . Can you provide any solution ??

    ReplyDelete
  7. Does this work in internet explorer?

    ReplyDelete
  8. Hi, is it possible to modify this awesome code will being upload directly using web api to another server?

    Thanks

    ReplyDelete
  9. This is not working on Safari browser in Mac.

    ReplyDelete
    Replies
    1. Did you get any solution for it? I have 90% of Target audience using Safari..Would be helpful if you can share anything to make it work in Safari

      Delete
  10. Dude you rule.....so many hours saved

    ReplyDelete
  11. Thanks a lot for this simple yet Powerful code, helped a lot.

    However it does not works in Safari, any help please?

    ReplyDelete
  12. Hi
    This does not work with IE9, IE9 does not understand blob object.Any workaround?

    ReplyDelete
    Replies
    1. You can use this polyfill https://github.com/eligrey/Blob.js

      Delete
  13. Its saved my day thank you so much...!!

    ReplyDelete
  14. This won't work in safari neither on ipads

    ReplyDelete
  15. Amazing! It works fine! Thank you so much!

    ReplyDelete
  16. Hi Jaliya. Thanks for tutorial.
    let filename = headers['filename'];
    in there filename is undefined. I checked browser developer tool the filename is in headers but i cannot get it. Any help will be great.

    ReplyDelete
    Replies
    1. Shoudln't it be,

      let filename = headers['x-filename'];

      Delete