I'm struggling with an issue with HapiJS 11.1.4 and replying with a stream to serve the browser a file. The resulting download is corrupt.
The main endpoint is /api/library/export
and uses ExcelJS to generate a stream containing data for an XLSX file, which I'm then replying with.
This endpoint works as expected when accessed using Curl and no corruption occurs. It is purely only the browser I'm having issues with.
Code for /api/library/export
endpoint:
handle(request, reply) {
// Invoke XLSX generation and reply
mapRecordsToXlsx(xlsxOpts).then(stream => {
console.log(stream.length);
return reply(stream)
// .type('application/octet-stream')
// .type('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
// .header('Content-Disposition', 'attachment; filename=library-export.xlsx;')
// .header('Content-Length', stream.length)
// .encoding('binary')
// .encoding(null)
});
}
As you can see, I've tried various combinations of response headers, to no avail.
Code for mapRecordsToXlsx
:
export function mapRecordsToXlsx(opts) {
const workbook = new Excel.stream.xlsx.WorkbookWriter();
workbook.created = new Date();
workbook.modified = new Date();
// Iterate through each set of mappings and records
opts.forEach(({title, mappings, types, records}) => {
// Create a worksheet for the current set
const sheet = workbook.addWorksheet(title);
// Set column headers from mapping
const headers = Object.keys(mappings);
sheet.columns = headers.map(header => {
return { header, key: mappings[header] };
});
// Generate rows for each record in the current sheet
records.forEach(record => {
const row = {};
headers.forEach(header => {
let value = _.get(record, mappings[header]);
// Detect custom types and apply appropriate transformations
if (types[header]) {
switch (types[header]) {
case 'date':
value = new Date(Date.parse(value)); // ExcelJS wants a Date object
break;
default:
break;
}
}
row[mappings[header]] = value;
});
sheet.addRow(row).commit();
});
sheet.commit();
});
return workbook.commit().then(() => {
return workbook.stream.read();
});
}
And, on the receiving side (EmberJS):
exportAll() {
const searchString = this.get('searchString');
if (searchString.length > 0) {
this.set('loading', true);
this.get('library').exportAll(searchString)
.then(xlsx => {
this.set('loading', false);
const fileName = `Library - ${searchString}.xlsx`;
// Hack to force download of file
const a = document.createElement('a');
document.body.appendChild(a);
a.style = 'display: none';
const blob = new Blob([xlsx], {type: 'octet/stream'});
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
return a.parentNode.removeChild(a);
});
}
}
this.get('library').exportAll(searchString)
is purely an abstraction over Ember.$.ajax()
which resolve/rejects as a promise.
A comparison of the resulting files, browser (left) vs curl (right):
The encoding looks wrong but I can't figure out how it is happening.
This may also help you to help me:
HTTP/1.1 200 OK
vary: origin,accept-encoding
access-control-allow-origin: http://localhost:4200
access-control-expose-headers: WWW-Authenticate,Server-Authorization
content-type: application/octet-stream
cache-control: no-cache
content-length: 10157
accept-ranges: bytes
Date: Tue, 15 Aug 2017 13:49:50 GMT
Connection: keep-alive
Content-Length matches what is being logged by HapiJS.