Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a downloadable file from a readableStream response in a fetch request

I have a react app that sends a POST request to my rails API. I want my API endpoint to generate a csv file and then to send back that file to the react app. I want then the browser to download of the csv file for the end user.

Here's how the endpoint looks like :

  def generate
      // next line builds the csv in tmp directory
      period_recap_csv = period_recap.build_csv
       // next line is supposed to send back the csv as response
      send_file Rails.root.join(period_recap.filepath), filename: period_recap.filename, type: 'text/csv'
    end

On the front end side here's how my request looks like :

export function generateCsvRequest(startDate, endDate) {
  fetch("http://localhost:3000/billing/finance-recaps/generate", {
    method: "post",
    headers: {
      Authorisation: `Token token=${authToken}`,
      'Accept': 'text/csv',
      'Content-Type': 'application/json',
      'X-Key-Inflection': 'camel',
    },
    //make sure to serialize your JSON body
    body: JSON.stringify({
      start_date: startDate,
      end_date: endDate
    })
  })
  .then( (response) => {
    console.log('resp', response);
    return response;
  }).then((data) => {
      // what should I do Here with the ReadableStream I get back ??
      console.log(data);
    }).catch(err => console.error(err));
}

As the response body I get a readableStream : enter image description here

What should I do now with that ReadableStream object to launch the download of that CSV file on the end user browser ?

like image 409
David Geismar Avatar asked Dec 18 '19 14:12

David Geismar


2 Answers

When you use the fetch API, your response object has a bunch of methods to handle the data that you get. The most used is json(). If you need to download a file coming from the server what you need is blob(), which works the same way as json().

response.blob().then(blob => download(blob))

There are a lot of npm packages to download files. file-saver is one of them. One way that works without dependencies though is by using the a tag. Something like that:

function download(blob, filename) {
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.style.display = 'none';
  a.href = url;
  // the filename you want
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  window.URL.revokeObjectURL(url);
}

Anyway, using a dependency would cover more edge cases, and it's usually more compatible. I hope that can be useful

If you want to show the pdf in another tab instead of downloading it, you would use window.open and pass the URL generated by window.URL.createObjectURL.

function showInOtherTab(blob) {
  const url = window.URL.createObjectURL(blob);
  window.open(url);
}
like image 177
Dylanbob211 Avatar answered Oct 04 '22 02:10

Dylanbob211


If the end point a stream, one solution is retrieve the whole file then to save it.

Disclaimer: the solution I offer below involve loading a whole pdf file in memory and avoid opening a new window tab to download the file (using downloadjs: very useful to rename and save the downloaded data). If anyone get a solution to avoid loading the whole file in memory before saving it, fell free to share :) :).

Here is the code:

import download from 'downloadjs'

fetch(...)
  .then(response => {
    //buffer to fill with all data from server
    let pdfContentBuffer = new Int8Array();

    // response.body is a readableStream 
    const reader = response.body.getReader();

    //function to retreive the next chunk from the stream
    function handleChunk({ done, chunk })  {
      if (done) {
        //everything has been loaded, call `download()` to save gthe file as pdf and name it "my-file.pdf"
        download(pdfContentBuffer, `my-file.pdf`, 'application/pdf')
        return;
      }

      // concat already loaded data with the loaded chunk
      pdfContentBuffer = Int8Array.from([...pdfContentBuffer, ...chunk]);

      // retreive next chunk
      reader.read().then(handleChunk);
    }

    //retreive first chunk
    reader.read().then(handleChunk)
  })
  .catch(err => console.error(err))

I share it hoping this will help others.

like image 20
Damien Leroux Avatar answered Oct 04 '22 02:10

Damien Leroux