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?

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.

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

The 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.

Join our newsletter – stay in the loop!

  • Bennie Copeland

    Please fix the „flares“ on the site. I found this article while browsing on my ipad, but the flares cover up the left hand part of the text. It is so annoying trying to make out the first word of each line that I just gave up entirely and had to stop reading what looked like an interesting article.

    • techdev_solutions

      thank you very much for your comment and sorry for the inconvenience! We fixed the style of the plugin so it should no longer stand in your way.

      Cheers, Alex

  • Jens X Augustsson

    Fine article, thanks!

    That’s a good reusable download directive. It appears though as no IE/Safari browser supports the download attribute ( – but you seem to use this in an actual product, so perhaps that’s not true?


    • techdev_solutions

      Hi Jens,

      sorry for the delay. First of all, trackr has been developed to suit our own needs. It is open source, but it’s not a product we’re selling. And all of our employees use either Firefox or Chrome so the approach works for us.

      I also just tried downloading a report in Safari Version 7.1 (9537. and it also worked. However, you are right IE doesn’t support this approach.

      Cheers, Alex

  • Abhi

    This is good one, Alex!

    Reached here while looking for a solution on downloading xlsx from a WebAPI server. How can this be extended to do the same?

    Your response is much awaited! :)


  • Blair Motchan

    Thanks, this is the exact solution I was looking for. Any thought about submitting this module to bower?

  • Yernar Amergaliyev

    What is the file size limit? I am having problem with files with size of 2Mb.

    • Could be your server and not this code. Most hosting companies have an arbitrary limit, but they are usually easy to change (if you know where).

  • Tristan Blackwell

    Thank you for sharing! You’ve split the event triggers and the anchor manipulation between the Controller and the Link methods, Is this because you shouldn’t change DOM elements from within the controller, or is there another reason?

    • Moritz Schulze

      Actually no, I don’t know why I did that. I guess to get access to the $http service, but that could also be done by injecting it directly into the directive and declaring the downloadPdf function in the link function.
      Not changing DOM in the controller is correct though.

  • Alan Wolman

    Thanks for sharing…this problem looked like being a real headache to solve.

    Any idea how we could get this to download automatically – rather than requiring the user to click on the button twice?

    • Moritz Schulze

      No, I didn’t find a solution to that when I was writing this directive. That is one of the downsides.

      • kbk

        just don’t set the href attribute, you can use‚data:application/pdf;…

        be careful though, you’ll have to avoid using $http.get which is async by nature, and the following will be blocked by default. i use modified, synchronized jquery.ajax() request instead.