84

Update 20140702:

(but I'm marking one of the other answers as accepted instead of my own, as it got me halfway there, and to reward the effort)


It appears that setting a HTTP request header is not possible through links with <a href="...">, and can only be done using XMLHttpRequest.

However, the URL linked to is a file that should be downloaded (browser should not navigate to its URL), and I am not sure is this can be done using AJAX.

Additionally, the file being returned is a binary file, and AJAX is not intended for that.

How would one go about triggering a file download with a HTTP request that has a custom header added to it?

edit: fix broken link

10
  • Why do you need a custom request header? You'll have to proxy this server-side. Commented Jul 1, 2014 at 1:49
  • that header is set by the server, not by JS. you can however easily ajax the binary file (except in old IE) and init a download of the blob/string in js. Commented Jul 1, 2014 at 1:49
  • 1
    @dandavis I need to set a custom header (containing a token) from the client - i.e. the HTTP GET request that the client makes to the server needs to contain the header, otherwise the server will simply return an error code. Commented Jul 1, 2014 at 2:26
  • 2
    xhr since v2 has supported xhr.responseType = 'blob'; you use url=window.URL.createObjectURL(xhr.response) to turn the blob into a url, set the href of an anchor to that url, set the anchors's download attrib to (ex) 'thefilename.ext', and call anchor.click() to move the file to the client's download folder. you can then call anchor.remove() to cleanup. Commented Jul 1, 2014 at 2:33
  • 2
    Why don't you just use the download attribute if you have a link to a file? Commented Jul 1, 2014 at 2:35

5 Answers 5

61

There are two ways to download a file where the HTTP request requires that a header be set.

The credit for the first goes to @guest271314, and credit for the second goes to @dandavis.

The first method is to use the HTML5 File API to create a temporary local file, and the second is to use base64 encoding in conjunction with a data URI.

The solution I used in my project uses the base64 encoding approach for small files, or when the File API is not available, otherwise using the the File API approach.

Solution:

        var id = 123;

        var req = ic.ajax.raw({
            type: 'GET',
            url: '/api/dowloads/'+id,
            beforeSend: function (request) {
                request.setRequestHeader('token', 'token for '+id);
            },
            processData: false
        });

        var maxSizeForBase64 = 1048576; //1024 * 1024

        req.then(
            function resolve(result) {
                var str = result.response;

                var anchor = $('.vcard-hyperlink');
                var windowUrl = window.URL || window.webkitURL;
                if (str.length > maxSizeForBase64 && typeof windowUrl.createObjectURL === 'function') {
                    var blob = new Blob([result.response], { type: 'text/bin' });
                    var url = windowUrl.createObjectURL(blob);
                    anchor.prop('href', url);
                    anchor.prop('download', id+'.bin');
                    anchor.get(0).click();
                    windowUrl.revokeObjectURL(url);
                }
                else {
                    //use base64 encoding when less than set limit or file API is not available
                    anchor.attr({
                        href: 'data:text/plain;base64,'+FormatUtils.utf8toBase64(result.response),
                        download: id+'.bin',
                    });
                    anchor.get(0).click();
                }

            }.bind(this),
            function reject(err) {
                console.log(err);
            }
        );

Note that I'm not using a raw XMLHttpRequest, and instead using ic-ajax, and should be quite similar to a jQuery.ajax solution.

Note also that you should substitute text/bin and .bin with whatever corresponds to the file type being downloaded.

The implementation of FormatUtils.utf8toBase64 can be found here

Sign up to request clarification or add additional context in comments.

6 Comments

Also worth mentioning is the point you make in your excellent blog post, about implementing this server side by creating a temporary download path that does not require a header after the server receives a regular request with the proper headers included.
Looks like you moved it again, here's the link: blog.bguiz.com/2014/07/03/…. Thanks for the detailed answer!
it works, still not very happy, because the file is downloaded silently first and does not appear in the downloads while downloading.
Does FileSaver or StreamSaver solve problem of large files? github.com/eligrey/FileSaver.js github.com/jimmywarting/StreamSaver.js
|
17

Try

html

<!-- placeholder , 
    `click` download , `.remove()` options ,
     at js callback , following js 
-->
<a>download</a>

js

        $(document).ready(function () {
            $.ajax({
                // `url` 
                url: '/echo/json/',
                type: "POST",
                dataType: 'json',
                // `file`, data-uri, base64
                data: {
                    json: JSON.stringify({
                        "file": "data:text/plain;base64,YWJj"
                    })
                },
                // `custom header`
                headers: {
                    "x-custom-header": 123
                },
                beforeSend: function (jqxhr) {
                    console.log(this.headers);
                    alert("custom headers" + JSON.stringify(this.headers));
                },
                success: function (data) {
                    // `file download`
                    $("a")
                        .attr({
                        "href": data.file,
                        "download": "file.txt"
                    })
                        .html($("a").attr("download"))
                        .get(0).click();
                    console.log(JSON.parse(JSON.stringify(data)));
                },
                error: function (jqxhr, textStatus, errorThrown) {
                  console.log(textStatus, errorThrown)
                }
            });
        });

jsfiddle http://jsfiddle.net/guest271314/SJYy3/

1 Comment

@ guest271314 Thanks for your answer, this works well for smaller files, but not so well for larger ones (> 1mb). I'm marking your solution as accepted, because it helped so much. My actual solution can be found here
16

I'm adding another option. The answers above were very useful for me, but I wanted to use jQuery instead of ic-ajax (it seems to have a dependency with Ember when I tried to install through bower). Keep in mind that this solution only works on modern browsers.

In order to implement this on jQuery I used jQuery BinaryTransport. This is a nice plugin to read AJAX responses in binary format.

Then you can do this to download the file and send the headers:

$.ajax({
    url: url,
    type: 'GET',
    dataType: 'binary',
    headers: headers,
    processData: false,
    success: function(blob) {
        var windowUrl = window.URL || window.webkitURL;
        var url = windowUrl.createObjectURL(blob);
        anchor.prop('href', url);
        anchor.prop('download', fileName);
        anchor.get(0).click();
        windowUrl.revokeObjectURL(url);
    }
});

The vars in the above script mean:

  • url: the URL of the file
  • headers: a Javascript object with the headers to send
  • fileName: the filename the user will see when downloading the file
  • anchor: it is a DOM element that is needed to simulate the download that must be wrapped with jQuery in this case. For example $('a.download-link').

2 Comments

Very clean and simple solution! +1
Simple and straightforward. and this might be helpful var anchor = $("<a></a>"); anchor.css("display", "none"); $("body").append(anchor); anchor.prop('href', url); anchor.prop('download', fileName); anchor.get(0).click(); windowUrl.revokeObjectURL(url); anchor.remove();
9

i want to post my solution here which was done AngularJS, ASP.NET MVC. The code illustrates how to download file with authentication.

WebApi method along with helper class:

[RoutePrefix("filess")]
class FileController: ApiController
{
    [HttpGet]
    [Route("download-file")]
    [Authorize(Roles = "admin")]
    public HttpResponseMessage DownloadDocument([FromUri] int fileId)
    {
        var file = "someFile.docx"// asking storage service to get file path with id
        return Request.ReturnFile(file);
    }
}

static class DownloadFIleFromServerHelper
{
    public static HttpResponseMessage ReturnFile(this HttpRequestMessage request, string file)
    {
        var result = request.CreateResponse(HttpStatusCode.OK);

        result.Content = new StreamContent(new FileStream(file, FileMode.Open, FileAccess.Read));
        result.Content.Headers.Add("x-filename", Path.GetFileName(file)); // letters of header names will be lowercased anyway in JS.
        result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
        {
            FileName = Path.GetFileName(file)
        };

        return result;
    }
}

Web.config file changes to allow sending file name in custom header.

<configuration>
    <system.webServer>
        <httpProtocol>
            <customHeaders>
                <add name="Access-Control-Allow-Methods" value="POST,GET,PUT,PATCH,DELETE,OPTIONS" />
                <add name="Access-Control-Allow-Headers" value="Authorization,Content-Type,x-filename" />
                <add name="Access-Control-Expose-Headers" value="Authorization,Content-Type,x-filename" />
                <add name="Access-Control-Allow-Origin" value="*" />

Angular JS Service Part:

function proposalService($http, $cookies, config, FileSaver) {
        return {
                downloadDocument: downloadDocument
        };

    function downloadFile(documentId, errorCallback) {
    $http({
        url: config.apiUrl + "files/download-file?documentId=" + documentId,
        method: "GET",
        headers: {
            "Content-type": "application/json; charset=utf-8",
            "Authorization": "Bearer " + $cookies.get("api_key")
        },
        responseType: "arraybuffer"  
        })
    .success( function(data, status, headers) {
        var filename = headers()['x-filename'];

        var blob = new Blob([data], { type: "application/octet-binary" });
        FileSaver.saveAs(blob, filename);
    })
    .error(function(data, status) {
        console.log("Request failed with status: " + status);
        errorCallback(data, status);
    });
};
};

Module dependency for FileUpload: angular-file-download (gulp install angular-file-download --save). Registration looks like below.

var app = angular.module('cool',
[
    ...
    require('angular-file-saver'),
])
. // other staff.

Comments

9

Pure jQuery.

$.ajax({
  type: "GET",
  url: "https://example.com/file",
  headers: {
    'Authorization': 'Bearer eyJraWQiFUDA.......TZxX1MGDGyg'
  },
  xhrFields: {
    responseType: 'blob'
  },
  success: function (blob) {
    var windowUrl = window.URL || window.webkitURL;
    var url = windowUrl.createObjectURL(blob);
    var anchor = document.createElement('a');
    anchor.href = url;
    anchor.download = 'filename.zip';
    anchor.click();
    anchor.parentNode.removeChild(anchor);
    windowUrl.revokeObjectURL(url);
  },
  error: function (error) {
    console.log(error);
  }
});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.