Downloading a PDF from a link in a browser – how hard can it be?

The Easy Way

Well, in case of a link with cookie-based authentication or a public one it’s quite easy: just place the link in the file. Maybe add some CSS to make it look good:

<a class="btn btn-primary" href="/some/path/to/a.pdf">Download</a>

The browser will happily send the session id if required – everything is fine.

But What If..

But what if you want or have to send custom headers? For example, the OAuth authorization header Authorization: Bearer 123904-affc-131239? A good example is a single page application that calls a REST API.

One approach I could think of is to generate download links that are public but not easy to guess and valid only for one download, like /199249aac66f8a822eb99cc185992/a.pdf. This skips the header part.

  • PRO: a simple <a> is enough in your frontend to download the PDF
  • CON: you have to organize all those links in your backend..

I frowned upon the work needed to be done in the backend and decided to do something different instead.

The Problem

Of course it is easy to execute the GET request with a header from within JavaScript. But what do you do with the result?

$.ajax('/some/path/to/a.pdf', {
    headers: {
        Authorization: 'Bearer 123904-affc-131239'
    })
    .done(function(data) {
        // now what?
    });

The solution I propose uses the HTML5 download attribute (see here). This needs a Base64 encoded PDF from the backend and embeds the data into href directly.

Since I am using AngularJS I wrote a directive for that – but the code should be easily transferable to a jQuery or even plain JavaScript solution.

The Solution

Without further ado, the template and code for the directive.

// jQuery needed, uses Bootstrap classes, adjust the path of templateUrl
app.directive('pdfDownload', function() {
    return {
        restrict: 'E',
        templateUrl: '/path/to/pdfDownload.tpl.html',
        scope: true,
        link: function(scope, element, attr) {
            var anchor = element.children()[0];

            // When the download starts, disable the link
            scope.$on('download-start', function() {
                $(anchor).attr('disabled', 'disabled');
            });

            // When the download finishes, attach the data to the link. Enable the link and change its appearance.
            scope.$on('downloaded', function(event, data) {
                $(anchor).attr({
                    href: 'data:application/pdf;base64,' + data,
                    download: attr.filename
                })
                    .removeAttr('disabled')
                    .text('Save')
                    .removeClass('btn-primary')
                    .addClass('btn-success');

                // Also overwrite the download pdf function to do nothing.
                scope.downloadPdf = function() {
                };
            });
        },
        controller: ['$scope', '$attrs', '$http', function($scope, $attrs, $http) {
            $scope.downloadPdf = function() {
                $scope.$emit('download-start');
                $http.get($attrs.url).then(function(response) {
                    $scope.$emit('downloaded', response.data);
                });
            };
        }]
});

Template:
<a href="" class="btn btn-primary" ng-click="downloadPdf()">Download</a>

Example:
<pdf-download url="/some/path/to/a.pdf" filename="my-awesome.pdf"></pdf-download>

This will display a blue button labeled Download to the user. When clicked, the PDF will be downloaded (Caution: the backend has to deliver the PDF in Base64 encoding!) and put into the href. The button turns green and switches the text to “Save”. The user can click again and will be presented with a standard download file dialog for the file my-awesome.pdf.

In my case the headers are always added by configuring $httpProvider, but you can set anything you like in line 34 of pdfdownload.js.

We are using this approach to download reports in trackr. You can see the it in action over at GitHub.

Stay in the Loop

If you would like to receive an email every now and then with new articles, just sign up below. We will never spam you!