Reputation: 31
I've spent days searching, tweaking, deploying, logging... Nothing has worked. I'm hoping it's just a stupid oversight on my part and someone who isn't buried in this project can just point it out to me.
I'm trying to use Puppeteer to create a PDF from an Node.js Express API endpoint hosted with Firebase Cloud Functions as a microservice for frontend Vue app. I'm using Axios to make a post request and passing an HTML string in the body. With bodyparser, I'm able to assign the data to a variable.
const express = require('express');
const functions = require('firebase-functions');
const puppeteer = require('puppeteer');
const bodyParser = require('body-parser');
const cors = require('cors');
const app = express();
const corsConfig = {
origin: true,
credentials: true,
}
app.use(cors(corsConfig));
app.options('*', cors(corsConfig));
app.all('*', async (request, response, next) => {
response.locals.browser = await puppeteer.launch({
dumpio: true,
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
next();
});
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.post('/pdf', async (request, response) => {
const html = request.body.html;
//console.log(typeof html, html);
if (!html) {
return response.status(400).send(
'Something happened so I cannot create your PDF right now.');
}
const browser = response.locals.browser;
const page = await browser.newPage();
//await page.goto(`data:text/html,${encodeURIComponent(html)}`, { waitUntil: 'networkidle0' });
await page.setContent(html, { waitUntil: ['domcontentloaded', 'load', "networkidle0"] }, { timeout: 0 });
await page.addStyleTag({ path: 'style.css' });
const pdf = await page.pdf({
format: 'Letter',
printBackground: true,
});
//console.log(pdf);
response.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdf.length });
response.send(pdf);
await browser.close();
return pdf;
});
const opts = { memory: '2GB', timeoutSeconds: 60 };
exports.pdf = functions.runWith(opts).https.onRequest(app);
Console logging shows me that the variable is a string, and the markup looks good. When I use it to page.setContent(html, {waitUntil: 'networkidle0'})
, then page.pdf()
, I get a PDF with 3 blank pages. Screenshot of Functions Log
I've tried the whole page.goto
thing and same results. If I change up the HTML string to something much shorter like '<h1>Hello World</h1>'
(tried both as variable and actual string), I'm given a PDF with 1 blank page. This leads me to believe it's understanding me, just not rendering. I changed all id's to class, and hex colors to RGB to avoid # conflicts (I've read they can mess things up).
I've tried using a handlebars template and only sending JSON data in the request body, and that didn't work either. Now, if I use postman and send the request with the HTML string in the body, I get back exactly what I'm looking for. The downside to this there it doesn't have AXIOS snippets for me to use and replicate the results within my app.
Here's the frontend
savePDF() {
this.getPDF()
.then(response => {
const blob = new Blob([response.data], { type: "application/pdf" });
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.target = "_blank";
link.click();
})
.catch(err => alert(err));
},
async getPDF() {
const html = document.querySelector(".wrapper").outerHTML;
axios.defaults.withCredentials = true;
const pdf = await axios
.post("MYENDPOINTURL/pdf", { html })
.then(
response => {
return response;
},
error => {
alert(error);
}
);
return pdf;
}
UPDATE I ditched Axios and used a fetch post request... and it worked! With that, I changed out the getPDF & savePDF front-end functions (server-side stayed the same) for this:
async savePDF() {
const html = document.querySelector(".wrapper").outerHTML;
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
var urlencoded = new URLSearchParams();
urlencoded.append("html", html);
var requestOptions = {
method: "POST",
headers: myHeaders,
body: urlencoded,
redirect: "follow"
};
fetch("MYENDPOINTURL/pdf", requestOptions)
.then(response => response.blob())
.then(result => {
const blob = new Blob([result], { type: "application/pdf" });
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.target = "_blank";
//link.download = `${this.propertyInfo.property}-${this.propertyInfo.workType}.pdf`;
link.click();
})
.catch(error => alert("error", error));
}
Upvotes: 2
Views: 2388
Reputation: 6267
I'm very late to the party, but in case someone is interested by making this work with Axios, just add {responseType: "blob"}
in the body of your request.
Example:
const res = await api.get(`/my-pdf`, { responseType: "blob" });
return res.data
The blob will be correctly generated.
Upvotes: 0
Reputation: 31
I'd still be interested in having Axios work, but switching that out for fetch and everything is up and running now.
const html = document.querySelector(".wrapper").outerHTML;
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
var urlencoded = new URLSearchParams();
urlencoded.append("html", html);
var requestOptions = {
method: "POST",
headers: myHeaders,
body: urlencoded,
redirect: "follow"
};
fetch("MYENDPOINTURL/pdf", requestOptions)
.then(response => response.blob())
.then(result => {
const blob = new Blob([result], { type: "application/pdf" });
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.target = "_blank";
//link.download = `${this.propertyInfo.property}-${this.propertyInfo.workType}.pdf`;
link.click();
})
.catch(error => alert("error", error));
Upvotes: 1