diff --git a/app/lib/headless-browser.js b/app/lib/headless-browser.js new file mode 100644 index 00000000..d149374c --- /dev/null +++ b/app/lib/headless-browser.js @@ -0,0 +1,39 @@ +const puppeteer = require('puppeteer'); + +const args = ['--no-startup-window']; +const userDataDir = './chromium-cache'; + +/** + * This class approach makes it easy to open multiple browser instances with + * different arguments in case that is ever required. + */ +class BrowserHandler { + constructor() { + const launchBrowser = async () => { + this.browser = false; + this.browser = await puppeteer.launch({ + headless: true, + devtools: false, + args, + userDataDir, + }); + this.browser.on('disconnected', launchBrowser); + }; + + (async () => { + await launchBrowser(); + })(); + } +} + +const getBrowser = (handler) => + new Promise((resolve) => { + const browserCheck = setInterval(() => { + if (handler.browser !== false) { + clearInterval(browserCheck); + resolve(handler.browser); + } + }, 100); + }); + +module.exports = { BrowserHandler, getBrowser }; diff --git a/app/lib/pdf.js b/app/lib/pdf.js index f25a482f..e5ab7603 100644 --- a/app/lib/pdf.js +++ b/app/lib/pdf.js @@ -1,11 +1,12 @@ /** * @module pdf */ +const { URL } = require('url'); const config = require('../models/config-model').server; +const { BrowserHandler, getBrowser } = require('./headless-browser'); +const browserHandler = new BrowserHandler(); const { timeout } = config.headless; -const puppeteer = require('puppeteer'); -const { URL } = require('url'); /** * @typedef PdfGetOptions @@ -35,35 +36,52 @@ const DEFAULTS = { * @param {PdfGetOptions} [options] - PDF options * @return { Promise } a promise that returns the PDF */ -async function get(url, options = {}) { +async function get( + url, + { + format = DEFAULTS.FORMAT, + margin = DEFAULTS.MARGIN, + landscape = DEFAULTS.LANDSCAPE, + scale = DEFAULTS.SCALE, + } = {} +) { if (!url) { throw new Error('No url provided'); } - options.format = options.format || DEFAULTS.FORMAT; - options.margin = options.margin || DEFAULTS.MARGIN; - options.landscape = options.landscape || DEFAULTS.LANDSCAPE; - options.scale = options.scale || DEFAULTS.SCALE; - const urlObj = new URL(url); - urlObj.searchParams.append('format', options.format); - urlObj.searchParams.append('margin', options.margin); - urlObj.searchParams.append('landscape', options.landscape); - urlObj.searchParams.append('scale', options.scale); + urlObj.searchParams.append('format', format); + urlObj.searchParams.append('margin', margin); + urlObj.searchParams.append('landscape', landscape); + urlObj.searchParams.append('scale', scale); - const browser = await puppeteer.launch({ headless: true }); + const browser = await getBrowser(browserHandler); const page = await browser.newPage(); let pdf; try { - await page + // To use an eventhandler here and catch a specific error, + // we have to return a Promise (in this case one that never resolves). + const detect401 = new Promise((resolve, reject) => { + page.on('requestfinished', (request) => { + if (request.response().status() === 401) { + const e = new Error('Authentication required'); + e.status = 401; + reject(e); + } + }); + }); + const goToPage = page .goto(urlObj.href, { waitUntil: 'networkidle0', timeout }) .catch((e) => { e.status = /timeout/i.test(e.message) ? 408 : 400; throw e; }); + // Either a 401 error is thrown or goto succeeds (or encounters a real loading error) + await Promise.race([detect401, goToPage]); + /* * This works around an issue with puppeteer not printing canvas * images that were loaded from a file. @@ -93,15 +111,15 @@ async function get(url, options = {}) { }); pdf = await page.pdf({ - landscape: options.landscape, - format: options.format, + landscape, + format, margin: { - top: options.margin, - left: options.margin, - right: options.margin, - bottom: options.margin, + top: margin, + left: margin, + right: margin, + bottom: margin, }, - scale: options.scale, + scale, printBackground: true, timeout, }); @@ -112,7 +130,6 @@ async function get(url, options = {}) { } await page.close(); - await browser.close(); return pdf; }