Downloading files from Ajax POST Requests

Occasionally I stumble upon the need to download files from POST requests. An example would be generating PDF files, where the PDF content is dependent on the request. Interestingly this is not as straightforward as you may think, but it's not that hard either.

A simple server

We're going to implement a really simple server which is generating PDFs from the POST request:

<?php
require_once 'vendor/autoload.php';

if($_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-type: application/pdf');
    http_response_code(200);

    // Contents
    $pdfContent = !empty($_POST['content']) ? $_POST['content'] : 'no content specified';
    
    // Generate the PDOF
    $pdf = new FPDF();
    $pdf->AddPage();
    $pdf->SetFont('Arial','B',16);
    $pdf->Cell(40,10, $pdfContent);
    
    return $pdf->Output(null, 'foobar-' . time() . '.pdf');
}

// Bad method
http_response_code(405);
exit();

Note: This code uses the FPDF library to generate PDF files. For demonstration purposes the pdf is filled with the content from $_POST['content'].

FPDF automatically takes care about setting the Content-Disposition to attachment. In case you don't use FPDF and need to set it manually simply add this before the output:

header('Content-Disposition: attachment; filename="filename.ext"');

Download the file

The more interesting thing about this is how the file is downloaded after sending the HTTP request. Let's dive straight into it:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Download POST Request</title>
</head>
<body>
Enter a text and click the button: <input type="text" id="content" value="Text for the generated pdf">
<button id="download">Send AJAX Request and download file</button>

<script>
  document.getElementById('download').addEventListener('click', function () {
    var content = document.getElementById('content').value;
    var request = new XMLHttpRequest();
    request.open('POST', '../server/', true);
    request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
    request.responseType = 'blob';

    request.onload = function() {
      // Only handle status code 200
      if(request.status === 200) {
        // Try to find out the filename from the content disposition `filename` value
        var disposition = request.getResponseHeader('content-disposition');
        var matches = /"([^"]*)"/.exec(disposition);
        var filename = (matches != null && matches[1] ? matches[1] : 'file.pdf');

        // The actual download
        var blob = new Blob([request.response], { type: 'application/pdf' });
        var link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = filename;

        document.body.appendChild(link);

        link.click();

        document.body.removeChild(link);
      }
      
      // some error handling should be done here...
    };

    request.send('content=' + content);
  });
</script>
</body>
</html>

Inhales deeply...

The actual download is done by creating a Blob object, which is used for a newly created a tag with a link to the created Blob object which is automatically clicked which ultimately opens the "Save file" dialog. Additionally it's appended to the body (which is a fix for Firefox) and is removed from the body afterwards (we don't want to have tons of invisible a tags on our body).

Well, as easy as vertically centering content in divs! (Flexbox is cheating!)

Keep in mind that this implementation uses plain JavaScript (to make it easier for everybody to follow the example), but the actual download works the same for most frameworks (jQuery, Vue, Angular, ...).

And, of course, you can find the entire implementation for this on GitHub.