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 :
What should I do now with that ReadableStream object to launch the download of that CSV file on the end user browser ?
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);
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With