From fc29b2fe763fea131432f1032cb6c48775a3bfe9 Mon Sep 17 00:00:00 2001 From: cvasseng Date: Wed, 29 Jul 2026 12:22:00 +0200 Subject: [PATCH 01/34] Add a load test harness Adds tests/other/load_test.js, a closed-loop saturation harness that drives a fixed number of concurrent virtual users against a running server, samples /health for pool state, and reports latency percentiles, the error breakdown and the peak acquire queue depth. The existing stress_test.js fires a fixed trickle of one request per 150ms and only logs the responses, so it cannot show the failure modes that appear under saturation. The new harness can reproduce them locally: - unbounded growth of the acquire queue - work continuing for clients that have already disconnected (--abort-after) - a server that stops answering altogether --- tests/other/load_test.js | 465 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 tests/other/load_test.js diff --git a/tests/other/load_test.js b/tests/other/load_test.js new file mode 100644 index 00000000..1a4499f5 --- /dev/null +++ b/tests/other/load_test.js @@ -0,0 +1,465 @@ +/******************************************************************************* + +Highcharts Export Server + +Copyright (c) 2016-2024, Highsoft + +Licenced under the MIT licence. + +Additionally a valid Highcharts license is required for use. + +See LICENSE file in root for details. + +*******************************************************************************/ + +/** + * Closed-loop saturation harness for the export server. + * + * Unlike stress_test.js (which fires a fixed trickle and only logs), this drives + * a fixed number of concurrent virtual users against the server, samples the + * /health endpoint for pool state, and reports latency percentiles, the error + * breakdown and the peak acquire queue depth. + * + * The point is to make the failure modes that only show up at scale observable + * locally: pool saturation, growing acquire queues, dropped requests and a + * server that stops answering altogether. + * + * Usage: + * node tests/other/load_test.js [--concurrency N] [--duration S] [--type png] + * [--url http://127.0.0.1:7801] [--series N] [--points N] + * [--client-timeout MS] [--abort-after MS] [--json out.json] + * + * Notable flags: + * --client-timeout abort the request from the client side after MS, the way + * an ALB or an impatient caller would. + * --abort-after abort every request after MS (models clients that give up + * while the server is still working on their chart). + */ + +import http from 'http'; +import https from 'https'; +import { writeFileSync } from 'fs'; + +import 'colors'; + +/** + * Parses `--key value` and `--flag` style arguments into an object. + * + * @param {string[]} argv - Raw process arguments. + * + * @returns {Object} Parsed arguments keyed by flag name. + */ +function parseArgs(argv) { + const args = {}; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + + if (!arg.startsWith('--')) { + continue; + } + + const key = arg.slice(2); + const next = argv[i + 1]; + + if (next === undefined || next.startsWith('--')) { + args[key] = true; + } else { + args[key] = next; + i++; + } + } + + return args; +} + +const args = parseArgs(process.argv.slice(2)); + +const config = { + url: args.url || 'http://127.0.0.1:7801', + concurrency: parseInt(args.concurrency || 10, 10), + duration: parseInt(args.duration || 30, 10) * 1000, + type: args.type || 'png', + series: parseInt(args.series || 3, 10), + points: parseInt(args.points || 25, 10), + clientTimeout: args['client-timeout'] + ? parseInt(args['client-timeout'], 10) + : 0, + abortAfter: args['abort-after'] ? parseInt(args['abort-after'], 10) : 0, + json: typeof args.json === 'string' ? args.json : null +}; + +const target = new URL(config.url); +const transport = target.protocol === 'https:' ? https : http; + +// Reuse sockets so we measure the server, not TCP handshakes +const agent = new transport.Agent({ + keepAlive: true, + maxSockets: config.concurrency + 8 +}); + +/** + * Builds a chart configuration of a controllable size, so the harness can model + * both cheap and expensive exports. + * + * @returns {Object} A Highcharts configuration object. + */ +function buildChart() { + const series = []; + + for (let s = 0; s < config.series; s++) { + const data = []; + + for (let p = 0; p < config.points; p++) { + // Deterministic, but varied enough to avoid trivially cacheable shapes + data.push(Math.round(Math.sin((s + 1) * p) * 100) / 2 + 50); + } + + series.push({ name: `Series ${s + 1}`, data }); + } + + return { + title: { text: 'Load test' }, + xAxis: { categories: Array.from({ length: config.points }, (_, i) => i) }, + series + }; +} + +const requestBody = JSON.stringify({ + type: config.type, + infile: buildChart() +}); + +// Collected results +const results = { + latencies: [], + ok: 0, + failed: 0, + aborted: 0, + byStatus: {}, + byError: {} +}; + +// Sampled pool state from /health +const poolSamples = []; +let healthFailures = 0; + +/** + * Increments a counter in a tally object. + * + * @param {Object} bucket - The tally object. + * @param {string} key - The key to increment. + */ +function tally(bucket, key) { + bucket[key] = (bucket[key] || 0) + 1; +} + +/** + * Issues a single export request and records its outcome. + * + * @returns {Promise} Resolves once the request has settled. + */ +function doRequest() { + return new Promise((resolve) => { + const start = process.hrtime.bigint(); + let settled = false; + + /** + * Records the outcome exactly once. + * + * @param {string} kind - One of 'ok', 'failed' or 'aborted'. + * @param {string} label - Status code or error label. + */ + const finish = (kind, label) => { + if (settled) { + return; + } + settled = true; + + const ms = Number(process.hrtime.bigint() - start) / 1e6; + results.latencies.push(ms); + + if (kind === 'ok') { + results.ok++; + tally(results.byStatus, label); + } else if (kind === 'aborted') { + results.aborted++; + } else { + results.failed++; + tally(results.byError, label); + } + + resolve(); + }; + + const request = transport.request( + { + hostname: target.hostname, + port: target.port, + path: target.pathname, + method: 'POST', + agent, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(requestBody) + } + }, + (response) => { + // Drain the body so the socket can be reused + let bytes = 0; + + response.on('data', (chunk) => { + bytes += chunk.length; + }); + + response.on('end', () => { + if (response.statusCode === 200 && bytes > 0) { + finish('ok', String(response.statusCode)); + } else { + finish('failed', `HTTP ${response.statusCode}`); + } + }); + + response.on('error', (error) => + finish('failed', error.code || 'stream') + ); + } + ); + + if (config.clientTimeout) { + request.setTimeout(config.clientTimeout, () => { + request.destroy(); + finish('failed', 'client-timeout'); + }); + } + + if (config.abortAfter) { + setTimeout(() => { + if (!settled) { + request.destroy(); + finish('aborted', 'aborted'); + } + }, config.abortAfter).unref(); + } + + request.on('error', (error) => { + // A deliberate abort surfaces here as ECONNRESET/socket hang up + if (settled) { + return; + } + finish('failed', error.code || error.message); + }); + + request.end(requestBody); + }); +} + +/** + * Samples the /health endpoint, recording pool state and any unavailability. + * + * @returns {Promise} Resolves once the sample has been taken. + */ +function sampleHealth() { + return new Promise((resolve) => { + const request = transport.request( + { + hostname: target.hostname, + port: target.port, + path: '/health', + method: 'GET', + agent + }, + (response) => { + let raw = ''; + + response.on('data', (chunk) => { + raw += chunk; + }); + + response.on('end', () => { + try { + const body = JSON.parse(raw); + poolSamples.push({ at: Date.now(), ...body.pool }); + } catch { + healthFailures++; + } + resolve(); + }); + } + ); + + request.setTimeout(5000, () => { + request.destroy(); + healthFailures++; + resolve(); + }); + + request.on('error', () => { + healthFailures++; + resolve(); + }); + + request.end(); + }); +} + +/** + * Returns the value at the given percentile of a numeric array. + * + * @param {number[]} sorted - A pre-sorted ascending array. + * @param {number} percentile - The percentile, between 0 and 100. + * + * @returns {number} The percentile value, rounded to whole milliseconds. + */ +function percentile(sorted, percentile) { + if (!sorted.length) { + return 0; + } + + const index = Math.min( + sorted.length - 1, + Math.ceil((percentile / 100) * sorted.length) - 1 + ); + + return Math.round(sorted[Math.max(0, index)]); +} + +/** + * Runs the load phase, then prints and optionally writes the report. + * + * @returns {Promise} Resolves once the run is reported. + */ +async function run() { + console.log( + 'Highcharts Export Server load test'.yellow.bold, + `\n target : ${config.url}`.green, + `\n concurrency : ${config.concurrency}`.green, + `\n duration : ${config.duration / 1000}s`.green, + `\n export type : ${config.type}`.green, + `\n payload : ${config.series} series x ${config.points} points`.green, + config.clientTimeout + ? `\n client tmo : ${config.clientTimeout}ms`.green + : '', + config.abortAfter ? `\n abort after : ${config.abortAfter}ms`.green : '', + '\n' + ); + + // Confirm the server is actually up before we start timing anything + await sampleHealth(); + + if (healthFailures) { + console.log( + `[ERROR] Could not reach ${config.url}/health.`.red, + 'Start the server before running this test.'.red + ); + process.exit(1); + } + + const deadline = Date.now() + config.duration; + const sampler = setInterval(sampleHealth, 500); + + /** + * A single virtual user, looping until the deadline. + * + * @returns {Promise} Resolves when the deadline passes. + */ + const worker = async () => { + while (Date.now() < deadline) { + await doRequest(); + } + }; + + const startedAt = Date.now(); + + await Promise.all(Array.from({ length: config.concurrency }, () => worker())); + + const elapsed = (Date.now() - startedAt) / 1000; + clearInterval(sampler); + + // Give the pool a moment to settle, then look at the drain behaviour + await new Promise((resolve) => setTimeout(resolve, 1500)); + await sampleHealth(); + + const sorted = [...results.latencies].sort((a, b) => a - b); + const total = results.ok + results.failed + results.aborted; + const maxPending = poolSamples.reduce( + (max, sample) => Math.max(max, sample.pending || 0), + 0 + ); + const maxUsed = poolSamples.reduce( + (max, sample) => Math.max(max, sample.used || 0), + 0 + ); + const finalPool = poolSamples[poolSamples.length - 1] || {}; + + const report = { + config, + elapsedSeconds: Number(elapsed.toFixed(1)), + requests: total, + ok: results.ok, + failed: results.failed, + aborted: results.aborted, + throughputPerSecond: Number((total / elapsed).toFixed(2)), + successRatePercent: total + ? Number(((results.ok / total) * 100).toFixed(2)) + : 0, + latencyMs: { + min: percentile(sorted, 0), + p50: percentile(sorted, 50), + p95: percentile(sorted, 95), + p99: percentile(sorted, 99), + max: percentile(sorted, 100) + }, + byStatus: results.byStatus, + byError: results.byError, + pool: { + maxPendingAcquires: maxPending, + maxUsed: maxUsed, + finalState: finalPool, + healthCheckFailures: healthFailures, + samples: poolSamples.length + } + }; + + console.log('Results'.yellow.bold); + console.log(` requests : ${report.requests}`); + console.log( + ` ok : ${report.ok} (${report.successRatePercent}%)`[ + report.failed || report.aborted ? 'yellow' : 'green' + ] + ); + console.log( + ` failed : ${report.failed}`[report.failed ? 'red' : 'green'] + ); + console.log(` aborted : ${report.aborted}`); + console.log(` throughput : ${report.throughputPerSecond} req/s`); + console.log( + ` latency ms : p50=${report.latencyMs.p50} p95=${report.latencyMs.p95} p99=${report.latencyMs.p99} max=${report.latencyMs.max}` + ); + console.log( + ` pool : maxUsed=${maxUsed} maxPending=${maxPending} final=${JSON.stringify(finalPool)}` + ); + console.log( + ` health misses : ${healthFailures}`[healthFailures ? 'red' : 'green'] + ); + + if (Object.keys(report.byError).length) { + console.log(' errors :'.red); + for (const [error, count] of Object.entries(report.byError)) { + console.log(` ${error}: ${count}`.red); + } + } + + if (config.json) { + writeFileSync(config.json, JSON.stringify(report, null, 2)); + console.log(`\n wrote ${config.json}`.green); + } + + // Leave the process free to exit + agent.destroy(); +} + +run().catch((error) => { + console.log(`[ERROR] ${error.stack}`.red); + process.exit(1); +}); From 47fccba8151bcd3c4e649c85a3b113e52abe9f54 Mon Sep 17 00:00:00 2001 From: cvasseng Date: Wed, 29 Jul 2026 12:26:16 +0200 Subject: [PATCH 02/34] Await page clearing before handing a worker to the next export Tarn's release() is synchronous: it runs the 'release' event handlers and then returns the resource to the free list in the same tick, without awaiting them. Clearing the page inside that handler therefore overlapped the next export whenever an acquire was already waiting, which is the normal case under saturation. The clearing is now started in the release handler but its promise is stored on the worker and awaited in factory.validate, which tarn does await. That keeps the work overlapped with the worker's idle time while making the handoff ordered. A page that fails to clear now returns false from validate, so tarn destroys and replaces the worker instead of exporting onto a page in an unknown state. Adds tests/other/page_isolation_test.js, which drives concurrent alternating SVG exports and asserts no response contains another chart's content. Note it passes both with and without this change: export.js already calls clearPageResources at the end of every export, which destroys the old charts and masks the overlap. The race was real but its visible effect was being covered by that second cleanup path, so this is a latent defect rather than an active one. The test is kept as a guard on the invariant. Measured no throughput cost: 19.17 req/s at concurrency 4 against a 18.95 req/s baseline, p50 unchanged at 200ms. --- CHANGELOG.md | 6 + lib/pool.js | 51 ++++++- tests/other/page_isolation_test.js | 225 +++++++++++++++++++++++++++++ 3 files changed, 276 insertions(+), 6 deletions(-) create mode 100644 tests/other/page_isolation_test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index bba5c2fe..ad36d8be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 6.0.0 + +_Fixes:_ + +- Ensured that a worker's page has finished being cleared before the worker is handed to the next export. The clearing was previously started when the worker was released but never awaited, so it overlapped the following export whenever one was already waiting for a worker. A page that cannot be cleared now recycles its worker instead of being exported onto. + # 5.1.0 _New Features:_ diff --git a/lib/pool.js b/lib/pool.js index 5b5f09e5..5516bed4 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -112,6 +112,41 @@ const factory = { return false; } + // NOTE: Wait for the page clearing started when this worker was released + // to complete before handing it out again. + // + // Tarn's release() is synchronous: it invokes the 'release' event + // handlers and then returns the resource to the free list within the + // same tick, without awaiting anything. Clearing the page in that + // handler therefore overlaps the next export whenever an acquire is + // already waiting, which is exactly the case under saturation. + // + // In practice the visible damage is currently limited, because + // export.js already calls clearPageResources at the end of every + // export, which destroys the old charts and removes the injected + // tags. The innerHTML reset done here is a second pass. But that + // makes correctness depend on the ordering of two independent cleanup + // paths, and an innerHTML reset landing part way through the next + // render would wipe its container. + // + // Tarn does await validate, so this is the point where the clearing + // is guaranteed to have finished. Doing it here rather than in the + // handler keeps the work overlapped with the worker's idle time, and + // lets a page that could not be cleared recycle the worker instead of + // being exported onto. + if (workerHandle.cleanPromise) { + const cleared = await workerHandle.cleanPromise; + workerHandle.cleanPromise = null; + + if (!cleared) { + log( + 3, + `[pool] Worker failed validation: the page could not be cleared after its previous export.` + ); + return false; + } + } + if ( poolConfig.workLimit && ++workerHandle.workCount > poolConfig.workLimit @@ -189,13 +224,17 @@ export const initPool = async (config) => { }); // Set events - pool.on('release', async (resource) => { - // Clear page - const r = await clearPage(resource.page, false); - log( - 4, - `[pool] Releasing a worker with ID ${resource.id}. Clear page status: ${r}.` + pool.on('release', (resource) => { + // Start clearing the page, but deliberately do not await it here - see + // the note in factory.validate, which is where the result is awaited + // before the worker can be handed out again. The catch is belt and braces: + // clearPage resolves false rather than rejecting, and nothing must be + // able to turn this into an unhandled rejection. + resource.cleanPromise = clearPage(resource.page, false).catch( + () => false ); + + log(4, `[pool] Releasing a worker with ID ${resource.id}.`); }); pool.on('destroySuccess', (eventId, resource) => { diff --git a/tests/other/page_isolation_test.js b/tests/other/page_isolation_test.js new file mode 100644 index 00000000..857e5e0f --- /dev/null +++ b/tests/other/page_isolation_test.js @@ -0,0 +1,225 @@ +/******************************************************************************* + +Highcharts Export Server + +Copyright (c) 2016-2024, Highsoft + +Licenced under the MIT licence. + +Additionally a valid Highcharts license is required for use. + +See LICENSE file in root for details. + +*******************************************************************************/ + +/** + * Checks that concurrent exports cannot see each other's chart state. + * + * Worker pages are reused between exports, and the page is cleared when a + * worker is released back to the pool. If that clearing is not guaranteed to + * have finished before the page is handed to the next export, then under + * saturation an export can render against the previous chart's state, or have + * its container wiped part way through rendering. + * + * This drives many concurrent SVG exports, alternating between two charts with + * distinguishable titles and series lengths, and asserts every response + * contains only its own chart. SVG is used because the output can be inspected + * directly as text. + * + * NOTE: This is a guard, not a reproducer. It was written alongside the fix that + * makes page clearing complete before a worker is handed out, and it + * passes both with and without that fix - because export.js separately + * calls clearPageResources at the end of every export, which destroys the + * old charts and so masks the overlap. It is kept because the invariant it + * checks is the one that matters, and it would catch a regression in + * either cleanup path. + * + * The server must be running, with the pool saturated by the concurrency used + * here for the check to be meaningful - a pool that is never contended will + * pass trivially. + * + * Usage: + * node tests/other/page_isolation_test.js [--concurrency N] [--rounds N] + * [--url http://127.0.0.1:7801] + */ + +import http from 'http'; + +import 'colors'; + +const args = process.argv.slice(2); + +/** + * Reads a `--key value` argument, falling back to a default. + * + * @param {string} name - The flag name, without dashes. + * @param {*} fallback - Value to use when the flag is absent. + * + * @returns {*} The parsed argument value. + */ +function arg(name, fallback) { + const index = args.indexOf(`--${name}`); + return index !== -1 && args[index + 1] ? args[index + 1] : fallback; +} + +const url = new URL(arg('url', 'http://127.0.0.1:7801')); +const concurrency = parseInt(arg('concurrency', 30), 10); +const rounds = parseInt(arg('rounds', 6), 10); + +const agent = new http.Agent({ + keepAlive: true, + maxSockets: concurrency + 4 +}); + +// Two charts that are trivially distinguishable in the rendered SVG. The point +// counts differ as well as the titles, so a partially cleared page shows up +// either as the wrong title or as the wrong number of data points. +const charts = [ + { + marker: 'ISOLATIONCHARTALPHA', + other: 'ISOLATIONCHARTBETA', + config: { + title: { text: 'ISOLATIONCHARTALPHA' }, + series: [{ data: [1, 2, 3] }] + } + }, + { + marker: 'ISOLATIONCHARTBETA', + other: 'ISOLATIONCHARTALPHA', + config: { + title: { text: 'ISOLATIONCHARTBETA' }, + series: [{ data: [10, 20, 30, 40, 50, 60, 70, 80] }] + } + } +]; + +/** + * Performs a single SVG export and checks the result against its own chart. + * + * @param {Object} chart - One of the entries of the charts array. + * + * @returns {Promise} Resolves to a result record describing the outcome. + */ +function exportAndCheck(chart) { + return new Promise((resolve) => { + const body = JSON.stringify({ type: 'svg', infile: chart.config }); + + const request = http.request( + { + hostname: url.hostname, + port: url.port, + path: '/', + method: 'POST', + agent, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body) + } + }, + (response) => { + let raw = ''; + + response.on('data', (chunk) => { + raw += chunk; + }); + + response.on('end', () => { + if (response.statusCode !== 200) { + // A rejected export is a capacity result, not an isolation failure + return resolve({ status: 'rejected', code: response.statusCode }); + } + + if (raw.includes(chart.other)) { + return resolve({ + status: 'contaminated', + detail: `response for ${chart.marker} contained ${chart.other}` + }); + } + + if (!raw.includes(chart.marker)) { + return resolve({ + status: 'missing', + detail: `response for ${chart.marker} did not contain its own title` + }); + } + + resolve({ status: 'ok' }); + }); + } + ); + + request.on('error', (error) => + resolve({ status: 'error', detail: error.code || error.message }) + ); + + request.end(body); + }); +} + +/** + * Runs the rounds of concurrent exports and reports the outcome. + * + * @returns {Promise} Resolves once the check has been reported. + */ +async function run() { + console.log( + 'Highcharts Export Server page isolation test'.yellow.bold, + `\n target : ${url.origin}`.green, + `\n concurrency : ${concurrency}`.green, + `\n rounds : ${rounds}`.green, + '\n' + ); + + const tally = { ok: 0, rejected: 0, contaminated: 0, missing: 0, error: 0 }; + const failures = []; + + for (let round = 0; round < rounds; round++) { + const batch = Array.from({ length: concurrency }, (_, i) => + exportAndCheck(charts[i % charts.length]) + ); + + for (const result of await Promise.all(batch)) { + tally[result.status]++; + + if ( + (result.status === 'contaminated' || result.status === 'missing') && + failures.length < 10 + ) { + failures.push(result.detail); + } + } + + console.log( + ` round ${round + 1}/${rounds}: ok=${tally.ok} rejected=${tally.rejected} contaminated=${tally.contaminated} missing=${tally.missing}` + ); + } + + console.log(''); + + if (tally.contaminated || tally.missing) { + console.log('[FAIL] Page state leaked between exports.'.red.bold); + for (const failure of failures) { + console.log(` ${failure}`.red); + } + process.exit(1); + } + + if (!tally.ok) { + console.log( + '[FAIL] No export succeeded, so isolation was never exercised.'.red.bold + ); + process.exit(1); + } + + console.log( + `[PASS] ${tally.ok} exports, none contaminated.`.green.bold, + tally.rejected ? `(${tally.rejected} rejected for capacity)`.yellow : '' + ); + + agent.destroy(); +} + +run().catch((error) => { + console.log(`[ERROR] ${error.stack}`.red); + process.exit(1); +}); From a3d3347ad7e0f37073b24b9fd6706692c824518c Mon Sep 17 00:00:00 2001 From: cvasseng Date: Wed, 29 Jul 2026 12:28:26 +0200 Subject: [PATCH 03/34] Close the page when setting it up fails newPage() created a browser page and then configured it without a try/catch, so a throw from any of the configuration steps left that page open for the lifetime of the browser. The caller only ever receives the error and never had a reference to the page, so nothing else could close it. setPageContent is the likely thrower, since it injects the entire Highcharts bundle and is therefore sensitive to a CPU starved instance. That is what makes this matter: on a sustained create failure the pool retries every createRetryInterval, 200ms by default, so every attempt leaked another browser tab until the browser ran out of memory. Note this is our leak, not tarn's. Tarn does destroy a resource that arrives after its own create timeout has fired (Pool.js:398), but that only covers the case where the factory resolves late, not where it throws. Adds tests/other/page_leak_test.js, which induces the failure by pointing the Highcharts cache path at a directory that does not exist and then counts the browser's open pages. Verified it fails before this change (5 attempts, 5 pages left open, exit 1) and passes after (0 pages left open, exit 0). It drives the browser module directly, so it does not need a running server. --- CHANGELOG.md | 1 + lib/browser.js | 44 ++++++++++++--- tests/other/page_leak_test.js | 102 ++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 tests/other/page_leak_test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ad36d8be..e981bf35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ _Fixes:_ - Ensured that a worker's page has finished being cleared before the worker is handed to the next export. The clearing was previously started when the worker was released but never awaited, so it overlapped the following export whenever one was already waiting for a worker. A page that cannot be cleared now recycles its worker instead of being exported onto. +- Fixed a resource leak where a browser page was left open if configuring it failed, for example when injecting the Highcharts scripts did not succeed. As the pool retries worker creation on an interval, a sustained failure leaked a browser page on every attempt until the browser ran out of memory. # 5.1.0 diff --git a/lib/browser.js b/lib/browser.js index f393359a..92330243 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -169,19 +169,45 @@ export async function newPage() { return false; } - // Create a page - const page = await browser.newPage(); + let page; - // Disable cache - await page.setCacheEnabled(false); + try { + // Create a page + page = await browser.newPage(); + + // Disable cache + await page.setCacheEnabled(false); + + // Set the content + await setPageContent(page); - // Set the content - await setPageContent(page); + // Set page events + setPageEvents(page); - // Set page events - setPageEvents(page); + return page; + } catch (error) { + // NOTE: Without this, a page created above but failing any of the + // subsequent steps is left open forever - the caller only receives + // the error and never had a reference to close it. setPageContent is + // the likely thrower, as it injects the entire Highcharts bundle and + // so is sensitive to a CPU starved instance. That matters because on + // a sustained create failure tarn retries every + // createRetryInterval (200ms by default), which would leak another + // browser tab on every attempt. + if (page && !page.isClosed()) { + try { + await page.close(); + } catch (closeError) { + logWithStack( + 2, + closeError, + '[browser] Could not close a page that failed to be set up.' + ); + } + } - return page; + throw error; + } } /** diff --git a/tests/other/page_leak_test.js b/tests/other/page_leak_test.js new file mode 100644 index 00000000..7eb0a3da --- /dev/null +++ b/tests/other/page_leak_test.js @@ -0,0 +1,102 @@ +/******************************************************************************* + +Highcharts Export Server + +Copyright (c) 2016-2024, Highsoft + +Licenced under the MIT licence. + +Additionally a valid Highcharts license is required for use. + +See LICENSE file in root for details. + +*******************************************************************************/ + +/** + * Checks that a page is not left open when setting it up fails. + * + * newPage() creates a browser page and then configures it, the expensive step + * being the injection of the Highcharts bundle. If any of the configuration + * steps throws, the caller receives the error but never had a reference to the + * page, so anything not closed here stays open for the lifetime of the browser. + * That matters because the pool retries creation on an interval, so a sustained + * create failure would leak a browser tab on every attempt. + * + * The failure is induced by pointing the Highcharts cache path at a directory + * that does not exist, which makes the script injection throw. + * + * Unlike the other scripts in this folder, this one does not need a running + * server - it drives the browser module directly. + * + * Usage: + * node tests/other/page_leak_test.js [--attempts N] + */ + +import { setOptions, getOptions } from '../../lib/config.js'; +import { create, newPage, close, get } from '../../lib/browser.js'; + +import 'colors'; + +const args = process.argv.slice(2); +const attemptsIndex = args.indexOf('--attempts'); +const attempts = + attemptsIndex !== -1 && args[attemptsIndex + 1] + ? parseInt(args[attemptsIndex + 1], 10) + : 5; + +console.log( + 'Highcharts Export Server page leak test'.yellow.bold, + `\n attempts : ${attempts}`.green, + '\n' +); + +// Load the default options, then launch a browser with them +setOptions({}, {}); +const options = getOptions(); + +await create(options.puppeteer?.args ?? []); +const browser = get(); + +const before = (await browser.pages()).length; +console.log(` pages after browser create : ${before}`); + +// Break the cache path so that injecting the Highcharts bundle fails +options.highcharts.cachePath = '.cache-does-not-exist-page-leak-test'; + +let failures = 0; + +for (let attempt = 0; attempt < attempts; attempt++) { + try { + await newPage(); + } catch { + failures++; + } +} + +const after = (await browser.pages()).length; + +console.log(` failed newPage() calls : ${failures}`); +console.log(` pages after failures : ${after}`); +console.log(''); + +await close(); + +if (failures !== attempts) { + console.log( + `[FAIL] Expected all ${attempts} attempts to fail, but ${attempts - failures} succeeded. The test is no longer inducing the failure it checks for.` + .red.bold + ); + process.exit(1); +} + +if (after > before) { + console.log( + `[FAIL] ${after - before} page(s) were left open by failed setup.`.red.bold + ); + process.exit(1); +} + +console.log( + `[PASS] No pages left open across ${attempts} failed setups.`.green.bold +); +process.exit(0); From 60d4f4fe4fa626a47ca00d04709baa127db64592 Mon Sep 17 00:00:00 2001 From: cvasseng Date: Wed, 29 Jul 2026 12:34:10 +0200 Subject: [PATCH 04/34] Recover when the browser process dies create() guarded the launch with `if (!browser)`, and nothing ever cleared that variable, so the guard could never fire again. When the browser process went away - an out of memory kill being the case that matters - the server stayed up but every export failed for the rest of its life, while /health continued to report healthy workers. On a load balanced deployment that makes the instance a black hole that still passes health checks, so autoscaling cannot route around it. The browser now emits into a disconnect handler that clears the reference and advances a generation counter, and newPage() relaunches when it finds no connected browser. Concurrent callers share one launch promise, since after a disconnect every pool worker discovers the missing browser at the same moment. The generation counter is what makes the pool recover. A page whose browser has gone away still returns false from isClosed(), so the existing validation cannot detect it and the pool keeps handing out pages belonging to a dead process - which is why the failure presented as an instant error rather than an acquire timeout. Workers are stamped with the generation they were created against and fail validation when it no longer matches, so tarn destroys and replaces them. close() now marks the shutdown as deliberate so that an intentional close is not mistaken for a crash and does not trigger a relaunch. Adds tests/other/browser_recovery_test.js. Verified it fails before this change (9 attempts across 30s, never recovered) and passes after (recovered after 2 attempts, 5/5 subsequent exports fine). It only kills a browser process parented to the server under test, so it cannot disturb an unrelated browser, and it skips itself on Windows. --- CHANGELOG.md | 1 + lib/browser.js | 218 ++++++++++++++++++------ lib/pool.js | 23 ++- tests/other/browser_recovery_test.js | 245 +++++++++++++++++++++++++++ 4 files changed, 438 insertions(+), 49 deletions(-) create mode 100644 tests/other/browser_recovery_test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e981bf35..4522707f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ _Fixes:_ +- Fixed an issue where the server never recovered if the browser process died, for example when killed by an out of memory reaper. The browser was launched once at startup and the guard preventing a second launch could never be cleared, so every export from that point failed while the pool continued to report healthy workers. The browser is now relaunched when it is found to be missing, and the workers holding pages from the dead browser are recognised as stale and replaced. This is detected by tracking which browser a worker was created against, because a page belonging to a browser that no longer exists still reports itself as open and so cannot be asked whether it is usable. - Ensured that a worker's page has finished being cleared before the worker is handed to the next export. The clearing was previously started when the worker was released but never awaited, so it overlapped the following export whenever one was already waiting for a worker. A page that cannot be cleared now recycles its worker instead of being exported onto. - Fixed a resource leak where a browser page was left open if configuring it failed, for example when injecting the Highcharts scripts did not succeed. As the pool retries worker creation on an interval, a sustained failure leaked a browser page on every attempt until the browser ran out of memory. diff --git a/lib/browser.js b/lib/browser.js index 92330243..ca13db30 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -30,6 +30,26 @@ const template = readFileSync(__dirname + '/templates/template.html', 'utf8'); let browser; +// Incremented every time the browser is lost. Pool workers are stamped with the +// value current when they were created, which lets the pool tell that a worker +// belongs to a browser that no longer exists. This is necessary because a page +// belonging to a dead browser still reports isClosed() === false, so the page +// itself cannot be asked whether it is usable. +let browserGeneration = 0; + +// The arguments the browser was last launched with, kept so that it can be +// relaunched on the same terms after an unexpected disconnect. +let lastPuppeteerArgs = []; + +// Set while the browser is being deliberately closed, so that the resulting +// disconnect is not mistaken for a crash. +let closingOnPurpose = false; + +// Shared promise for an in-flight launch, so that concurrent callers - the pool +// creating several workers at once, typically - trigger a single launch rather +// than one each. +let launchPromise = null; + /** * Retrieves the existing Puppeteer browser instance. * @@ -46,6 +66,50 @@ export function get() { return browser; } +/** + * Returns the current browser generation. Pool workers are stamped with this + * value on creation and compared against it on validation, so that workers + * holding a page from a previous, now dead, browser can be identified and + * replaced. + * + * @returns {number} The current browser generation. + */ +export function getGeneration() { + return browserGeneration; +} + +/** + * Reports whether a usable browser is currently connected. + * + * @returns {boolean} True when a browser exists and is connected. + */ +export function isConnected() { + return !!browser?.connected; +} + +/** + * Handles the browser disconnecting. Puppeteer emits this when the browser + * process goes away for any reason, including being killed by an out of memory + * reaper, which is the case this exists for. + * + * The browser reference is cleared so that the guard in create() will actually + * relaunch it, and the generation is advanced so that every pool worker holding + * a page from the dead browser fails validation and is replaced. + */ +function handleDisconnect() { + if (closingOnPurpose) { + return; + } + + browserGeneration++; + browser = undefined; + + log( + 1, + `[browser] The browser disconnected unexpectedly. Invalidating all workers and relaunching on next use (generation ${browserGeneration}).` + ); +} + /** * Creates a Puppeteer browser instance with the specified arguments. * @@ -58,6 +122,43 @@ export function get() { * instance are reached, or if no browser instance is found after retries. */ export async function create(puppeteerArgs) { + // Remember the arguments so that an unexpected disconnect can relaunch the + // browser on the same terms, and clear any deliberate-close state left from a + // previous cycle + if (puppeteerArgs !== undefined) { + lastPuppeteerArgs = puppeteerArgs; + } + closingOnPurpose = false; + + if (browser?.connected) { + return browser; + } + + // NOTE: Concurrent callers must share a single launch. The pool creates + // several workers at once, and after a disconnect each of them finds + // no browser at the same moment - without this they would launch a + // browser each. + if (!launchPromise) { + launchPromise = launchBrowser(lastPuppeteerArgs).finally(() => { + launchPromise = null; + }); + } + + return launchPromise; +} + +/** + * Launches a Puppeteer browser instance, retrying on failure. + * + * @param {Array} puppeteerArgs - Additional arguments for Puppeteer launch. + * + * @returns {Promise} A Promise resolving to the Puppeteer browser + * instance. + * + * @throws {ExportError} Throws an ExportError if max retries to open a browser + * instance are reached, or if no browser instance is found after retries. + */ +async function launchBrowser(puppeteerArgs) { // Get debug and other options const { puppeteer: puppeteerOptions, debug, other } = getOptions(); @@ -76,66 +177,67 @@ export async function create(puppeteerArgs) { ...(enabledDebug && debugOptions) }; - // Create a browser - if (!browser) { - const maxTries = 25; - let tryCount = 0; + const maxTries = 25; + let tryCount = 0; - const open = async () => { - try { + const open = async () => { + try { + log( + 3, + `[browser] Attempting to get a browser instance (try ${++tryCount}).` + ); + browser = await puppeteer.launch(launchOptions); + } catch (error) { + // This isn't a full error yet as puppeteer sometimes takes time to + // initialize properly. + logWithStack( + 2, + error, + `[browser] Failed to launch a browser instance - retrying (attempt ${tryCount}/${maxTries}).` + ); + + // Retry to launch browser until reaching max attempts + if (tryCount < maxTries) { log( 3, - `[browser] Attempting to get a browser instance (try ${++tryCount}).` + `[browser] Retry to open a browser (attempt ${tryCount}/${maxTries}).` ); - browser = await puppeteer.launch(launchOptions); - } catch (error) { - // This isn't a full error yet as puppeteer sometimes takes time to - // initialize properly. - logWithStack( - 2, - error, - `[browser] Failed to launch a browser instance - retrying (attempt ${tryCount}/${maxTries}).` - ); - - // Retry to launch browser until reaching max attempts - if (tryCount < 25) { - log( - 3, - `[browser] Retry to open a browser (attempt ${tryCount}/${maxTries}).` - ); - await new Promise((response) => setTimeout(response, 4000)); - await open(); - } else { - //... now it's an error, which is caught by the caller - throw error; - } + await new Promise((response) => setTimeout(response, 4000)); + await open(); + } else { + //... now it's an error, which is caught by the caller + throw error; } - }; - - try { - await open(); + } + }; - // Shell mode inform - if (launchOptions.headless === 'shell') { - log(3, `[browser] Launched browser in shell mode.`); - } + try { + await open(); - // Debug mode inform - if (enabledDebug) { - log(3, `[browser] Launched browser in debug mode.`); - } - } catch (error) { - throw new ExportError( - '[browser] Maximum retries to open a browser instance reached.' - ).setError(error); + // Shell mode inform + if (launchOptions.headless === 'shell') { + log(3, `[browser] Launched browser in shell mode.`); } - if (!browser) { - throw new ExportError('[browser] Cannot find a browser to open.'); + // Debug mode inform + if (enabledDebug) { + log(3, `[browser] Launched browser in debug mode.`); } + } catch (error) { + throw new ExportError( + '[browser] Maximum retries to open a browser instance reached.' + ).setError(error); } - // Return a browser promise + if (!browser) { + throw new ExportError('[browser] Cannot find a browser to open.'); + } + + // Notice the browser going away, so that the pool's workers can be + // invalidated and the browser relaunched, rather than the pool handing out + // pages belonging to a process that no longer exists + browser.once('disconnected', handleDisconnect); + return browser; } @@ -146,10 +248,17 @@ export async function create(puppeteerArgs) { * is closed. */ export async function close() { + // Mark this as intentional so the resulting disconnect is not treated as a + // crash and does not trigger a relaunch + closingOnPurpose = true; + // Close the browser when connnected if (browser?.connected) { await browser.close(); } + + browser = undefined; + log(4, '[browser] Closed the browser.'); } @@ -161,10 +270,21 @@ export async function close() { * The function creates a new page, disables caching, sets content using * setPageContent(), and returns the created Puppeteer Page. * + * If the browser is not currently available it is relaunched first, so that the + * pool can recover by itself after the browser process has gone away. + * * @returns {(boolean|object)} Returns false if the browser instance is not * available, or a Puppeteer Page object representing the newly created page. */ export async function newPage() { + // The browser may have gone away since the last page was made. Bring it back + // rather than failing every export from here on - create() is guarded, so + // concurrent callers share the one relaunch. + if (!browser?.connected && !closingOnPurpose) { + log(3, '[browser] No browser available, attempting to relaunch it.'); + await create(); + } + if (!browser) { return false; } @@ -468,6 +588,8 @@ function setPageEvents(page) { export default { get, + getGeneration, + isConnected, create, close, newPage, diff --git a/lib/pool.js b/lib/pool.js index 5516bed4..1915a26c 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -19,7 +19,8 @@ import { create as createBrowser, close as closeBrowser, newPage, - clearPage + clearPage, + getGeneration as getBrowserGeneration } from './browser.js'; import puppeteerExport from './export.js'; import { log, logWithStack } from './logger.js'; @@ -80,6 +81,9 @@ const factory = { return { id, page, + // Which browser this page belongs to, so that it can be recognised as + // stale if that browser goes away - see factory.validate + generation: getBrowserGeneration(), // Try to distribute the initial work count workCount: Math.round(Math.random() * (poolConfig.workLimit / 2)) }; @@ -112,6 +116,23 @@ const factory = { return false; } + // NOTE: A page whose browser has gone away still reports + // isClosed() === false, so the check above cannot detect it and the + // pool would keep handing out pages belonging to a dead process, + // failing every export while continuing to report healthy workers. + // + // The generation is the reliable signal: it advances whenever the + // browser is lost, so any worker created against an earlier one is + // stale. Returning false here makes tarn destroy the worker and + // create a replacement, which relaunches the browser via newPage. + if (workerHandle.generation !== getBrowserGeneration()) { + log( + 3, + `[pool] Worker failed validation: it belongs to a browser that is gone (worker generation ${workerHandle.generation}, current ${getBrowserGeneration()}).` + ); + return false; + } + // NOTE: Wait for the page clearing started when this worker was released // to complete before handing it out again. // diff --git a/tests/other/browser_recovery_test.js b/tests/other/browser_recovery_test.js new file mode 100644 index 00000000..4d8dc823 --- /dev/null +++ b/tests/other/browser_recovery_test.js @@ -0,0 +1,245 @@ +/******************************************************************************* + +Highcharts Export Server + +Copyright (c) 2016-2024, Highsoft + +Licenced under the MIT licence. + +Additionally a valid Highcharts license is required for use. + +See LICENSE file in root for details. + +*******************************************************************************/ + +/** + * Checks that the server recovers when the browser process is killed. + * + * This is the failure that happens in production when the browser is killed by + * an out of memory reaper. A page belonging to a dead browser still reports + * isClosed() === false, so without the generation check the pool cannot tell + * that its workers are stale and keeps handing them out, failing every export + * indefinitely while still reporting healthy workers. + * + * The test performs an export, kills the browser, and then requires exports to + * start succeeding again within a timeout. + * + * NOTE: This test kills processes. It only ever kills a browser process that is + * a direct child of the export server it was pointed at, found by parent + * pid, so it will not touch an unrelated browser. It is POSIX only and + * skips itself elsewhere. It is not part of any automated suite - run it + * deliberately against a server started for the purpose. + * + * Usage: + * node tests/other/browser_recovery_test.js [--url http://127.0.0.1:7801] + * [--server-pid N] [--timeout MS] + */ + +import { execSync } from 'child_process'; + +import 'colors'; + +const args = process.argv.slice(2); + +/** + * Reads a `--key value` argument, falling back to a default. + * + * @param {string} name - The flag name, without dashes. + * @param {*} fallback - Value to use when the flag is absent. + * + * @returns {*} The parsed argument value. + */ +function arg(name, fallback) { + const index = args.indexOf(`--${name}`); + return index !== -1 && args[index + 1] ? args[index + 1] : fallback; +} + +const url = arg('url', 'http://127.0.0.1:7801'); +const recoveryTimeout = parseInt(arg('timeout', 60000), 10); + +if (process.platform === 'win32') { + console.log( + '[SKIP] This test relies on POSIX process tools and does not run on Windows.' + .yellow + ); + process.exit(0); +} + +console.log( + 'Highcharts Export Server browser recovery test'.yellow.bold, + `\n target : ${url}`.green, + `\n timeout : ${recoveryTimeout}ms`.green, + '\n' +); + +const chart = { type: 'png', infile: { series: [{ data: [1, 2, 3] }] } }; + +/** + * Attempts a single export. + * + * @returns {Promise} True when the export succeeded. + */ +async function tryExport() { + try { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(chart), + signal: AbortSignal.timeout(30000) + }); + + if (!response.ok) { + return false; + } + + return (await response.arrayBuffer()).byteLength > 0; + } catch { + return false; + } +} + +/** + * Finds candidate pids for the export server process. + * + * More than one process can match, because a shell wrapper's command line + * contains the server's command line too. All matches are returned and the + * browser is then located as a child of any of them, which sidesteps having to + * work out which is the real one. + * + * @returns {number[]} The candidate server pids. + */ +function findServerPids() { + const explicit = arg('server-pid', null); + + if (explicit) { + return [parseInt(explicit, 10)]; + } + + const pids = execSync('ps -eo pid,args', { encoding: 'utf8' }) + .split('\n') + .filter( + (line) => line.includes('bin/cli.js') && !line.includes('recovery_test') + ) + .map((line) => parseInt(line.trim().split(/\s+/)[0], 10)) + .filter((pid) => !Number.isNaN(pid) && pid !== process.pid); + + if (!pids.length) { + throw new Error( + 'Could not find the export server process. Pass --server-pid explicitly.' + ); + } + + return pids; +} + +/** + * Finds browser processes that are direct children of any of the given pids. + * + * Matching on the parent is what keeps this safe: an unrelated browser running + * on the machine is never a child of the export server. + * + * @param {number[]} parentPids - Candidate parent process ids. + * + * @returns {number[]} The matching child pids. + */ +function findBrowserChildren(parentPids) { + return execSync('ps -eo pid,ppid,args', { encoding: 'utf8' }) + .split('\n') + .filter((line) => { + const parts = line.trim().split(/\s+/); + return ( + parentPids.includes(parseInt(parts[1], 10)) && + /chrom(e|ium)/i.test(line) && + !/crashpad/i.test(line) + ); + }) + .map((line) => parseInt(line.trim().split(/\s+/)[0], 10)); +} + +/** + * Runs the recovery check. + * + * @returns {Promise} Resolves once the check has been reported. + */ +async function run() { + if (!(await tryExport())) { + console.log( + '[FAIL] The server could not export before the browser was killed. Start a healthy server first.' + .red.bold + ); + process.exit(1); + } + console.log(' export before kill : ok'.green); + + const serverPids = findServerPids(); + const browserPids = findBrowserChildren(serverPids); + + if (!browserPids.length) { + console.log( + `[FAIL] Found no browser process parented to the server (candidate pids ${serverPids.join(', ')}).` + .red.bold + ); + process.exit(1); + } + + for (const pid of browserPids) { + console.log(` killing browser : pid ${pid}`); + try { + process.kill(pid, 'SIGKILL'); + } catch (error) { + console.log(` could not kill ${pid}: ${error.message}`.yellow); + } + } + + const deadline = Date.now() + recoveryTimeout; + let attempts = 0; + let recovered = false; + + while (Date.now() < deadline) { + attempts++; + + if (await tryExport()) { + recovered = true; + break; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + console.log(''); + + if (!recovered) { + console.log( + `[FAIL] The server did not recover within ${recoveryTimeout}ms (${attempts} attempts). Exports are still failing.` + .red.bold + ); + process.exit(1); + } + + // A recovered server must also keep working + let consecutive = 0; + + for (let i = 0; i < 5; i++) { + if (await tryExport()) { + consecutive++; + } + } + + if (consecutive < 5) { + console.log( + `[FAIL] Recovered, but only ${consecutive}/5 subsequent exports succeeded.` + .red.bold + ); + process.exit(1); + } + + console.log( + `[PASS] Recovered after ${attempts} attempt(s), and 5/5 subsequent exports succeeded.` + .green.bold + ); +} + +run().catch((error) => { + console.log(`[ERROR] ${error.stack}`.red); + process.exit(1); +}); From aa18bc100adb08fe5fde9cc7393f2ba95b6e7ed9 Mon Sep 17 00:00:00 2001 From: cvasseng Date: Wed, 29 Jul 2026 12:41:30 +0200 Subject: [PATCH 05/34] Add machine readable error codes to error responses Every internal failure is reported as HTTP 400, the same status as a malformed request, so nothing downstream could tell a capacity problem from callers sending bad data. That matters because the two need opposite responses: one means scale up or shed load, the other means fix the caller. The message text was the only signal, which is not something a dashboard or a client can branch on. Errors now optionally carry an errorCode, surfaced as a property of the error response. The codes live in lib/errors/codes.js with notes on what each one means for a caller, and are treated as a stable contract. ExportError.setError carries a code up from a wrapped error, since errors are wrapped as they travel up the stack and the reason would otherwise be lost at the first wrap. Status codes are deliberately unchanged - this server must not return 5xx. The property is omitted entirely when an error carries no code, so existing response shapes are untouched. Verified against a running server: a request with no body and one with no chart data both return EXPORT_INVALID_REQUEST, a valid export still returns 200 with the image, and under saturation 99 of 99 captured failures returned EXPORT_ACQUIRE_TIMEOUT. --- CHANGELOG.md | 4 +++ lib/errors/ExportError.js | 18 +++++++++++++- lib/errors/HttpError.js | 4 +-- lib/errors/codes.js | 49 +++++++++++++++++++++++++++++++++++++ lib/pool.js | 10 +++++--- lib/server/error.js | 15 ++++++++++-- lib/server/routes/export.js | 13 +++++++--- 7 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 lib/errors/codes.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 4522707f..d0bdf52b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ _Fixes:_ - Ensured that a worker's page has finished being cleared before the worker is handed to the next export. The clearing was previously started when the worker was released but never awaited, so it overlapped the following export whenever one was already waiting for a worker. A page that cannot be cleared now recycles its worker instead of being exported onto. - Fixed a resource leak where a browser page was left open if configuring it failed, for example when injecting the Highcharts scripts did not succeed. As the pool retries worker creation on an interval, a sustained failure leaked a browser page on every attempt until the browser ran out of memory. +_New Features:_ + +- Added an `errorCode` property to error responses, so that a request refused because the server was busy can be told apart from one refused because it was malformed. Both are reported with the same status code, which previously left the message text as the only way to distinguish them. The codes are `EXPORT_INVALID_REQUEST`, `EXPORT_QUEUE_FULL`, `EXPORT_ACQUIRE_TIMEOUT`, `EXPORT_RASTERIZATION_TIMEOUT` and `EXPORT_FAILED`, and may be relied upon by callers. Status codes and the rest of the response body are unchanged, and the property is absent on errors that carry no code. + # 5.1.0 _New Features:_ diff --git a/lib/errors/ExportError.js b/lib/errors/ExportError.js index be659551..1328d723 100644 --- a/lib/errors/ExportError.js +++ b/lib/errors/ExportError.js @@ -1,8 +1,12 @@ class ExportError extends Error { - constructor(message) { + constructor(message, errorCode = false) { super(); this.message = message; this.stackMessage = message; + + if (errorCode) { + this.errorCode = errorCode; + } } setError(error) { @@ -13,12 +17,24 @@ class ExportError extends Error { if (error.statusCode) { this.statusCode = error.statusCode; } + // NOTE: Carry a machine readable code up from the wrapped error. Errors are + // wrapped as they travel up the stack, and without this the reason a + // request failed would be lost at the first wrap, leaving only the + // message to tell a capacity problem from a bad request. + if (error.errorCode && !this.errorCode) { + this.errorCode = error.errorCode; + } if (error.stack) { this.stackMessage = error.message; this.stack = error.stack; } return this; } + + setCode(errorCode) { + this.errorCode = errorCode; + return this; + } } export default ExportError; diff --git a/lib/errors/HttpError.js b/lib/errors/HttpError.js index b995a4d2..0a11b4e6 100644 --- a/lib/errors/HttpError.js +++ b/lib/errors/HttpError.js @@ -1,8 +1,8 @@ import ExportError from './ExportError.js'; class HttpError extends ExportError { - constructor(message, status) { - super(message); + constructor(message, status, errorCode = false) { + super(message, errorCode); this.status = this.statusCode = status; } diff --git a/lib/errors/codes.js b/lib/errors/codes.js new file mode 100644 index 00000000..c6929187 --- /dev/null +++ b/lib/errors/codes.js @@ -0,0 +1,49 @@ +/******************************************************************************* + +Highcharts Export Server + +Copyright (c) 2016-2024, Highsoft + +Licenced under the MIT licence. + +Additionally a valid Highcharts license is required for use. + +See LICENSE file in root for details. + +*******************************************************************************/ + +/** + * Machine readable error codes, returned as the `errorCode` property of an + * error response. + * + * These exist because the HTTP status code cannot carry the distinction that + * matters most in production: a request refused because the server was busy and + * a request refused because it was malformed are both reported as 400. Without a + * code, a dashboard cannot separate a capacity problem from callers sending bad + * data, and a client cannot tell whether retrying is worthwhile. + * + * Treat these as a stable contract - callers may branch on them. + */ +export const errorCodes = { + // The request itself was not usable: missing body, no chart data, or content + // that is not allowed. Retrying without changing the request will not help. + INVALID_REQUEST: 'EXPORT_INVALID_REQUEST', + + // The server was already holding as many queued exports as it is willing to, + // and refused this one without starting work on it. Retrying later, ideally + // with backoff, is appropriate. + QUEUE_FULL: 'EXPORT_QUEUE_FULL', + + // No worker became available within the acquire timeout. Same meaning for a + // caller as QUEUE_FULL, but reached by waiting rather than by being refused + // up front. + ACQUIRE_TIMEOUT: 'EXPORT_ACQUIRE_TIMEOUT', + + // The chart was too large or complex to render within the allotted time. + RASTERIZATION_TIMEOUT: 'EXPORT_RASTERIZATION_TIMEOUT', + + // The export failed for a reason that is not one of the above. + EXPORT_FAILED: 'EXPORT_FAILED' +}; + +export default errorCodes; diff --git a/lib/pool.js b/lib/pool.js index 1915a26c..21fefea4 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -27,6 +27,7 @@ import { log, logWithStack } from './logger.js'; import { measureTime } from './utils.js'; import ExportError from './errors/ExportError.js'; +import { errorCodes } from './errors/codes.js'; // The pool instance let pool = false; @@ -367,7 +368,8 @@ export const postWork = async (chart, options) => { (options.payload?.requestId ? `For request with ID ${options.payload?.requestId} - ` : '') + - `Error encountered when acquiring an available entry: ${acquireCounter()}ms.` + `Error encountered when acquiring an available entry: ${acquireCounter()}ms.`, + errorCodes.ACQUIRE_TIMEOUT ).setError(error); } log(4, '[pool] Acquired a worker handle.'); @@ -409,13 +411,15 @@ export const postWork = async (chart, options) => { result.message === 'Rasterization timeout' ) { throw new ExportError( - 'Rasterization timeout: your chart may be too complex or large, and failed to render within the allotted time.' + 'Rasterization timeout: your chart may be too complex or large, and failed to render within the allotted time.', + errorCodes.RASTERIZATION_TIMEOUT ).setError(result); } else { throw new ExportError( (options.payload?.requestId ? `For request with ID ${options.payload?.requestId} - ` - : '') + `Error encountered during export: ${exportCounter()}ms.` + : '') + `Error encountered during export: ${exportCounter()}ms.`, + errorCodes.EXPORT_FAILED ).setError(result); } } diff --git a/lib/server/error.js b/lib/server/error.js index eaeef9f9..dbe22844 100644 --- a/lib/server/error.js +++ b/lib/server/error.js @@ -32,11 +32,22 @@ const logErrorMiddleware = (error, req, res, next) => { */ const returnErrorMiddleware = (error, req, res, next) => { // Gather all requied information for the response - const { statusCode: stCode, status, message, stack } = error; + const { statusCode: stCode, status, message, stack, errorCode } = error; const statusCode = stCode || status || 400; // Set and return response - res.status(statusCode).json({ statusCode, message, stack }); + // + // NOTE: The errorCode is only included when the error carries one, so that + // the response shape is unchanged for errors that do not. It exists + // because the status code alone cannot distinguish a request the server + // refused because it was busy from one it refused because it was + // malformed - both are reported as 400. + res.status(statusCode).json({ + statusCode, + message, + stack, + ...(errorCode ? { errorCode } : {}) + }); }; export default (app) => { diff --git a/lib/server/routes/export.js b/lib/server/routes/export.js index 4a3c656c..6e850da0 100644 --- a/lib/server/routes/export.js +++ b/lib/server/routes/export.js @@ -27,6 +27,7 @@ import { } from '../../utils.js'; import HttpError from '../../errors/HttpError.js'; +import { errorCodes } from '../../errors/codes.js'; // Reversed MIME types const reversedMime = { @@ -109,7 +110,8 @@ const exportHandler = async (request, response, next) => { if (!body || isObjectEmpty(body)) { throw new HttpError( 'The request body is required. Please ensure that your Content-Type header is correct (accepted types are application/json and multipart/form-data).', - 400 + 400, + errorCodes.INVALID_REQUEST ); } @@ -138,7 +140,8 @@ const exportHandler = async (request, response, next) => { throw new HttpError( "No correct chart data found. Ensure that you are using either application/json or multipart/form-data headers. If sending JSON, make sure the chart data is in the 'infile', 'options', or 'data' attribute. If sending SVG, ensure it is in the 'svg' attribute.", - 400 + 400, + errorCodes.INVALID_REQUEST ); } @@ -217,7 +220,8 @@ const exportHandler = async (request, response, next) => { if (body.svg && isPrivateRangeUrlFound(options.payload.svg)) { throw new HttpError( 'SVG potentially contain at least one forbidden URL in xlink:href element. Please review the SVG content and ensure that all referenced URLs comply with security policies.', - 400 + 400, + errorCodes.INVALID_REQUEST ); } @@ -251,7 +255,8 @@ const exportHandler = async (request, response, next) => { if (!info || !info.result) { throw new HttpError( `Unexpected return from chart generation. Please check your request data. For the request with ID ${uniqueId}, the result is ${info.result}.`, - 400 + 400, + errorCodes.EXPORT_FAILED ); } From 5388fc672b41b6d184b21f3a5cbb04c0e7827c31 Mon Sep 17 00:00:00 2001 From: cvasseng Date: Wed, 29 Jul 2026 12:56:05 +0200 Subject: [PATCH 06/34] Bound the export queue and refuse work beyond it The acquire queue was unbounded. Measured against the previous behaviour, 150 concurrent clients drove the queue to 144 waiting exports, and the server accepted more than twenty times the work it could complete: 58.9% of requests succeeded, the rest failing only after a full 5 second acquire timeout, with each one holding its parsed body - up to maxUploadSize - in memory while it waited. That memory pressure is what eventually gets the browser killed, at which point the previous commit's recovery path is all that saves the instance. Throughput does not improve past the pool size. Measured, it peaks at about 19 exports/s around concurrency 4 and then falls, so a deeper queue cannot buy capacity - only latency and memory. The limit defaults to four times maxWorkers, 32 with default settings, and requests beyond it are refused before express.json parses the body, so a refusal is cheap. The refusal is deliberately delayed, which is the non-obvious part. A first attempt refused instantly and made things much worse: clients that retry the moment they are refused pushed the request rate to 11000/s, the event loop went entirely to producing refusals, and goodput collapsed from ~19 exports/s to 0.65. The 5 second acquire timeout had been providing backpressure by accident, simply by making every client wait before it could retry. queueRejectDelay puts that back on purpose, at a fraction of the cost, holding only a socket and a timer. The timer is cleared if the client disconnects first. Measured, PNG, default pool: concurrency 4 18.95 req/s p50 200ms -> 19.03 req/s p50 200ms concurrency 40 15.82 goodput -> 16.18 goodput, 95.9% ok concurrency 150 15.58 goodput -> 15.26 goodput 58.9% ok, p50 5105ms -> p50 502ms peak queue 144 -> peak queue 32 Goodput is preserved, the queue is bounded, and the latency of a refusal drops by an order of magnitude. Adds --min-goodput and --max-queue assertions to the load harness so this is a gate rather than something to eyeball. Verified the same invocation fails against the unbounded queue (peak 144, exit 1) and passes with the limit in place (peak 32). --- .env.sample | 2 ++ CHANGELOG.md | 6 ++++ lib/envs.js | 2 ++ lib/pool.js | 78 ++++++++++++++++++++++++++++++++++++++-- lib/schemas/config.js | 28 +++++++++++++++ lib/server/server.js | 68 +++++++++++++++++++++++++++++++++++ tests/other/load_test.js | 38 +++++++++++++++++++- 7 files changed, 219 insertions(+), 3 deletions(-) diff --git a/.env.sample b/.env.sample index aef40f74..530bb74a 100644 --- a/.env.sample +++ b/.env.sample @@ -57,6 +57,8 @@ SERVER_SSL_CERT_PATH = POOL_MIN_WORKERS = 4 POOL_MAX_WORKERS = 8 POOL_WORK_LIMIT = 40 +POOL_QUEUE_LIMIT = 0 +POOL_QUEUE_REJECT_DELAY = 500 POOL_ACQUIRE_TIMEOUT = 5000 POOL_CREATE_TIMEOUT = 5000 POOL_DESTROY_TIMEOUT = 5000 diff --git a/CHANGELOG.md b/CHANGELOG.md index d0bdf52b..63ff4429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 6.0.0 +_Breaking Changes:_ + +- Exports now queue only up to a bounded limit, and requests arriving beyond it are refused instead of being queued. Previously the queue was unbounded, so a saturated server accepted far more work than it could complete, held each waiting request's parsed body in memory, and then failed a large share of them once they exceeded the acquire timeout. With default pool settings the limit is 32, and refusals are now returned in about half a second rather than after a five second wait. See `queueLimit` and `queueRejectDelay` below for tuning. + _Fixes:_ - Fixed an issue where the server never recovered if the browser process died, for example when killed by an out of memory reaper. The browser was launched once at startup and the guard preventing a second launch could never be cleared, so every export from that point failed while the pool continued to report healthy workers. The browser is now relaunched when it is found to be missing, and the workers holding pages from the dead browser are recognised as stale and replaced. This is detected by tracking which browser a worker was created against, because a page belonging to a browser that no longer exists still reports itself as open and so cannot be asked whether it is usable. @@ -8,6 +12,8 @@ _Fixes:_ _New Features:_ +- Added the `POOL_QUEUE_LIMIT`/`--queueLimit`/`queueLimit` option, capping how many exports may wait for a worker, and defaulting to four times `maxWorkers`. Requests arriving beyond the limit are refused before their body is parsed, so a refused request costs almost nothing. The rationale is that export throughput does not improve past the pool size, so queueing beyond it adds latency and memory use without adding capacity. Raise it to accept deeper queues at the cost of higher latency under load. +- Added the `POOL_QUEUE_REJECT_DELAY`/`--queueRejectDelay`/`queueRejectDelay` option, defaulting to 500 milliseconds, which is how long the server waits before answering a request it is refusing for capacity. This is deliberate backpressure. Answering instantly lets clients that retry immediately raise the request rate by orders of magnitude, at which point the server spends its whole event loop refusing requests and starves the exports already in progress. The acquire timeout used to provide this throttling as a side effect of making clients wait; bounding the queue removes that, so the delay restores it explicitly and far more cheaply, holding only a socket rather than a parsed body and a queue slot. Set it to 0 to answer immediately, which is only advisable when something upstream is limiting the request rate. - Added an `errorCode` property to error responses, so that a request refused because the server was busy can be told apart from one refused because it was malformed. Both are reported with the same status code, which previously left the message text as the only way to distinguish them. The codes are `EXPORT_INVALID_REQUEST`, `EXPORT_QUEUE_FULL`, `EXPORT_ACQUIRE_TIMEOUT`, `EXPORT_RASTERIZATION_TIMEOUT` and `EXPORT_FAILED`, and may be relied upon by callers. Status codes and the rest of the response body are unchanged, and the property is absent on errors that carry no code. # 5.1.0 diff --git a/lib/envs.js b/lib/envs.js index ed8c3d23..b55df215 100644 --- a/lib/envs.js +++ b/lib/envs.js @@ -194,6 +194,8 @@ export const Config = z.object({ POOL_MIN_WORKERS: v.nonNegativeNum(), POOL_MAX_WORKERS: v.nonNegativeNum(), POOL_WORK_LIMIT: v.positiveNum(), + POOL_QUEUE_LIMIT: v.nonNegativeNum(), + POOL_QUEUE_REJECT_DELAY: v.nonNegativeNum(), POOL_ACQUIRE_TIMEOUT: v.nonNegativeNum(), POOL_CREATE_TIMEOUT: v.nonNegativeNum(), POOL_DESTROY_TIMEOUT: v.nonNegativeNum(), diff --git a/lib/pool.js b/lib/pool.js index 21fefea4..5ecf0890 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -39,11 +39,55 @@ export const stats = { exportFromSvgAttempts: 0, timeSpent: 0, droppedExports: 0, - spentAverage: 0 + spentAverage: 0, + rejectedForCapacity: 0 }; let poolConfig = {}; +// The resolved maximum number of exports allowed to wait for a worker +let queueLimit = 0; + +/** + * Resolves the queue limit from the pool configuration. + * + * A limit of 0 means derive it from the pool size. Four times maxWorkers keeps + * the worst case wait at roughly four exports' worth of time, which is a + * meaningful bound, while leaving enough slack to absorb normal bursts. + * + * @param {Object} config - The pool section of the configuration. + * + * @returns {number} The resolved queue limit. + */ +const resolveQueueLimit = (config) => { + const configured = parseInt(config.queueLimit); + const maxWorkers = parseInt(config.maxWorkers); + + if (!isNaN(configured) && configured > 0) { + return configured; + } + + return (isNaN(maxWorkers) ? 8 : maxWorkers) * 4; +}; + +/** + * Returns the number of exports currently allowed to wait for a worker. + * + * @returns {number} The active queue limit. + */ +export const getQueueLimit = () => queueLimit; + +/** + * Returns how long to wait before answering a request refused because the queue + * was full. + * + * @returns {number} The delay in milliseconds. + */ +export const getQueueRejectDelay = () => { + const delay = parseInt(poolConfig.queueRejectDelay); + return isNaN(delay) || delay < 0 ? 0 : delay; +}; + const factory = { /** * Creates a new worker page for the export pool. @@ -210,6 +254,9 @@ export const initPool = async (config) => { // For the module scope usage poolConfig = config && config.pool ? { ...config.pool } : {}; + // Work out how deep the queue of waiting exports is allowed to get + queueLimit = resolveQueueLimit(poolConfig); + // Create a browser instance with the puppeteer arguments await createBrowser(config.puppeteerArgs); @@ -347,6 +394,32 @@ export const postWork = async (chart, options) => { throw new ExportError('Work received, but pool has not been started.'); } + // NOTE: Refuse the work rather than queue it when the queue is already at + // its limit. + // + // Throughput does not improve past the pool size - measured, it peaks + // at roughly the number of workers and then falls - so an unbounded + // queue cannot buy capacity. What it does buy is latency and memory: + // every waiting request holds its parsed body until it is served or + // times out, and a saturated server was observed accepting more than + // twenty times the work it could complete, queueing thousands of + // requests and then failing most of them on timeout. + // + // Failing here is cheap and immediate, and carries a distinct error + // code so that a caller and a dashboard can tell this apart from a + // malformed request. + if (pool.numPendingAcquires() >= queueLimit) { + ++stats.rejectedForCapacity; + + throw new ExportError( + (options.payload?.requestId + ? `For request with ID ${options.payload?.requestId} - ` + : '') + + `The server is at capacity: ${pool.numPendingAcquires()} exports are already waiting for a worker (limit is ${queueLimit}). Please retry shortly.`, + errorCodes.QUEUE_FULL + ); + } + // Acquire the worker along with the id of resource and work count const acquireCounter = measureTime(); try { @@ -485,7 +558,8 @@ export const getPoolInfoJSON = () => ({ all: pool.numFree() + pool.numUsed(), available: pool.numFree(), used: pool.numUsed(), - pending: pool.numPendingAcquires() + pending: pool.numPendingAcquires(), + queueLimit }); /** diff --git a/lib/schemas/config.js b/lib/schemas/config.js index 64342b19..f92c4a4b 100644 --- a/lib/schemas/config.js +++ b/lib/schemas/config.js @@ -536,6 +536,20 @@ export const defaultConfig = { description: 'The number of work pieces that can be performed before restarting the worker process.' }, + queueLimit: { + value: 0, + type: 'number', + envLink: 'POOL_QUEUE_LIMIT', + description: + 'The maximum number of exports allowed to wait for a worker. Requests arriving beyond this are refused instead of being queued. Set to 0 to derive it as four times maxWorkers. Throughput does not improve past the pool size, so a deeper queue only adds latency and memory use.' + }, + queueRejectDelay: { + value: 500, + type: 'number', + envLink: 'POOL_QUEUE_REJECT_DELAY', + description: + 'The duration, in milliseconds, to wait before answering a request refused because the queue is full. This is deliberate backpressure: answering instantly lets clients that retry immediately consume the whole event loop refusing them, starving the exports already in progress. Set to 0 to answer immediately, only advisable when something upstream is limiting the request rate.' + }, acquireTimeout: { value: 5000, type: 'number', @@ -997,6 +1011,20 @@ export const promptsConfig = { 'The pieces of work that can be performed before restarting a Puppeteer process', initial: defaultConfig.pool.workLimit.value }, + { + type: 'number', + name: 'queueLimit', + message: + 'The maximum number of exports allowed to wait for a worker, or 0 to derive it from maxWorkers', + initial: defaultConfig.pool.queueLimit.value + }, + { + type: 'number', + name: 'queueRejectDelay', + message: + 'The number of milliseconds to wait before refusing a request because the queue is full', + initial: defaultConfig.pool.queueRejectDelay.value + }, { type: 'number', name: 'acquireTimeout', diff --git a/lib/server/server.js b/lib/server/server.js index 530251aa..745d4d1c 100644 --- a/lib/server/server.js +++ b/lib/server/server.js @@ -24,8 +24,17 @@ import multer from 'multer'; import errorHandler from './error.js'; import rateLimit from './rate_limit.js'; import { log, logWithStack } from '../logger.js'; +import { + getPool, + getQueueLimit, + getQueueRejectDelay, + stats as poolStats +} from '../pool.js'; import { __dirname } from '../utils.js'; +import { errorCodes } from '../errors/codes.js'; +import HttpError from '../errors/HttpError.js'; + import vSwitchRoute from './routes/change_hc_version.js'; import exportRoutes from './routes/export.js'; import healthRoute from './routes/health.js'; @@ -106,6 +115,65 @@ export const startServer = async (serverConfig) => { } }); + // NOTE: Refuse work before the body is parsed when the queue is already + // full. Checking only inside the pool would mean a request that is + // going to be refused anyway has its body - up to maxUploadSize, 3MiB + // by default - read and held in memory first. With a deep queue that + // is exactly the memory pressure a saturated server cannot afford, + // and it is what eventually gets the browser killed. + app.use((request, response, next) => { + // Only export requests are worth gating; the admin version route is not + // pool work and must stay reachable when the server is busy + if (request.method !== 'POST' || request.path.startsWith('/version/')) { + return next(); + } + + const pool = getPool(); + + if (pool && pool.numPendingAcquires() >= getQueueLimit()) { + ++poolStats.rejectedForCapacity; + + const error = new HttpError( + `The server is at capacity: ${pool.numPendingAcquires()} exports are already waiting for a worker (limit is ${getQueueLimit()}). Please retry shortly.`, + 400, + errorCodes.QUEUE_FULL + ); + + // NOTE: Do not answer immediately by default. Measured: with an instant + // refusal, clients that retry as soon as they are refused drive + // the request rate up by orders of magnitude - 150 concurrent + // clients reached 11000 requests per second - and the server then + // spends its entire event loop refusing them, starving the exports + // already in progress. Goodput fell from ~19 exports per second to + // under 1. + // + // The 5 second acquire timeout used to provide this backpressure + // accidentally, by making every client wait before it could retry. + // Bounding the queue removes that, so the delay puts it back + // deliberately, and far more cheaply: no body has been parsed and + // no worker is held, only a socket and a timer. + const delay = getQueueRejectDelay(); + + if (!delay) { + return next(error); + } + + const onClose = () => clearTimeout(timer); + + const timer = setTimeout(() => { + response.removeListener('close', onClose); + next(error); + }, delay); + + // Do not keep a timer alive for a client that has already gone + response.once('close', onClose); + + return; + } + + next(); + }); + // Enable body parser app.use(express.json({ limit: uploadLimitBytes })); app.use(express.urlencoded({ extended: true, limit: uploadLimitBytes })); diff --git a/tests/other/load_test.js b/tests/other/load_test.js index 1a4499f5..5fb6f5a2 100644 --- a/tests/other/load_test.js +++ b/tests/other/load_test.js @@ -86,7 +86,10 @@ const config = { ? parseInt(args['client-timeout'], 10) : 0, abortAfter: args['abort-after'] ? parseInt(args['abort-after'], 10) : 0, - json: typeof args.json === 'string' ? args.json : null + json: typeof args.json === 'string' ? args.json : null, + // Assertions, so this can be used as a gate rather than only for reading + minGoodput: args['min-goodput'] ? parseFloat(args['min-goodput']) : 0, + maxQueue: args['max-queue'] ? parseInt(args['max-queue'], 10) : 0 }; const target = new URL(config.url); @@ -457,6 +460,39 @@ async function run() { // Leave the process free to exit agent.destroy(); + + // Assertions. Goodput is what matters under overload: the number of exports + // actually completed per second, as distinct from the request rate, which a + // server refusing everything instantly can make arbitrarily high. + const goodput = report.ok / elapsed; + const failures = []; + + if (config.minGoodput && goodput < config.minGoodput) { + failures.push( + `goodput was ${goodput.toFixed(2)} exports/s, expected at least ${config.minGoodput}` + ); + } + + if (config.maxQueue && maxPending > config.maxQueue) { + failures.push( + `peak queue reached ${maxPending}, expected at most ${config.maxQueue}` + ); + } + + if (config.minGoodput || config.maxQueue) { + console.log(''); + console.log(` goodput : ${goodput.toFixed(2)} exports/s`); + + if (failures.length) { + console.log('[FAIL]'.red.bold); + for (const failure of failures) { + console.log(` ${failure}`.red); + } + process.exit(1); + } + + console.log('[PASS] assertions met'.green.bold); + } } run().catch((error) => { From 37ef1c174efb7ffc314e24e1e27fbd3d0da1f29d Mon Sep 17 00:00:00 2001 From: cvasseng Date: Wed, 29 Jul 2026 13:12:23 +0200 Subject: [PATCH 07/34] Guarantee error responses never carry a 5xx status This server must never answer with a 5xx. Nothing here sets one deliberately - the only statuses in the codebase are 200, 400, 401 and 429 - but the status of an error is not always ours to begin with. setError copies statusCode up from a wrapped error, and wrapped errors include ones raised while fetching from the CDN, which can carry whatever status a remote returned. Clamping at the single point every error response passes through makes a 5xx structurally impossible rather than something every future call site has to be careful about. Anything outside 1xx-4xx is answered as 400 and logged, since arriving there means an error carried a status it should not have. An error raised after the response has already begun now ends the response instead of being passed on. There is no status left to set at that point, and handing it onwards let the framework's own handler answer, which answers 500. Also reverts ExportError's constructor to taking only a message. Giving it a second parameter for the error code was wrong: cache.js:146 and cache.js:181 already pass a number there, intending a status, which the constructor has always ignored. Accepting a second parameter silently gave those a meaning and would have put numeric errorCode values into responses. Codes are now set with an explicit setCode() instead, and HttpError keeps its own unambiguous third parameter. Adds tests/unit/server_error.test.js, the first unit coverage of the server layer, including a sweep asserting no status from 100 to 599 produces a 5xx response. Verified 8 of its tests fail without the clamp and all pass with it. Verified against a running server: a valid export returns 200, a malformed request returns 400 with EXPORT_INVALID_REQUEST, and 214 refusals under saturation all returned 400 with EXPORT_QUEUE_FULL. No clamp warnings were logged, so nothing attempted a 5xx in normal operation. --- CHANGELOG.md | 1 + lib/errors/ExportError.js | 11 +-- lib/errors/HttpError.js | 6 +- lib/pool.js | 26 ++++--- lib/server/error.js | 33 +++++++- tests/unit/server_error.test.js | 132 ++++++++++++++++++++++++++++++++ 6 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 tests/unit/server_error.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ff4429..224b8e3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ _Fixes:_ - Fixed an issue where the server never recovered if the browser process died, for example when killed by an out of memory reaper. The browser was launched once at startup and the guard preventing a second launch could never be cleared, so every export from that point failed while the pool continued to report healthy workers. The browser is now relaunched when it is found to be missing, and the workers holding pages from the dead browser are recognised as stale and replaced. This is detected by tracking which browser a worker was created against, because a page belonging to a browser that no longer exists still reports itself as open and so cannot be asked whether it is usable. - Ensured that a worker's page has finished being cleared before the worker is handed to the next export. The clearing was previously started when the worker was released but never awaited, so it overlapped the following export whenever one was already waiting for a worker. A page that cannot be cleared now recycles its worker instead of being exported onto. - Fixed a resource leak where a browser page was left open if configuring it failed, for example when injecting the Highcharts scripts did not succeed. As the pool retries worker creation on an interval, a sustained failure leaked a browser page on every attempt until the browser ran out of memory. +- Ensured error responses can never carry a status outside the 1xx to 4xx range. The status of an error is not always set locally, as it is carried up from wrapped errors, and those include errors from outbound HTTP requests which can hold any status a remote returned. Anything outside the range is now answered as 400 and logged. Additionally, an error raised after the response has already begun now ends the response instead of being passed on, which previously allowed the framework's own handler to answer in its place. _New Features:_ diff --git a/lib/errors/ExportError.js b/lib/errors/ExportError.js index 1328d723..7853c3b8 100644 --- a/lib/errors/ExportError.js +++ b/lib/errors/ExportError.js @@ -1,12 +1,13 @@ class ExportError extends Error { - constructor(message, errorCode = false) { + // NOTE: Deliberately takes only a message. Two existing call sites in cache.js + // pass a number as a second argument, intending a status code, which + // this constructor has always ignored. Accepting a second parameter here + // would silently give those a meaning. Use setCode() instead, which is + // explicit. + constructor(message) { super(); this.message = message; this.stackMessage = message; - - if (errorCode) { - this.errorCode = errorCode; - } } setError(error) { diff --git a/lib/errors/HttpError.js b/lib/errors/HttpError.js index 0a11b4e6..01f89e7c 100644 --- a/lib/errors/HttpError.js +++ b/lib/errors/HttpError.js @@ -2,8 +2,12 @@ import ExportError from './ExportError.js'; class HttpError extends ExportError { constructor(message, status, errorCode = false) { - super(message, errorCode); + super(message); this.status = this.statusCode = status; + + if (errorCode) { + this.errorCode = errorCode; + } } setStatus(status) { diff --git a/lib/pool.js b/lib/pool.js index 5ecf0890..fe1dab69 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -415,9 +415,8 @@ export const postWork = async (chart, options) => { (options.payload?.requestId ? `For request with ID ${options.payload?.requestId} - ` : '') + - `The server is at capacity: ${pool.numPendingAcquires()} exports are already waiting for a worker (limit is ${queueLimit}). Please retry shortly.`, - errorCodes.QUEUE_FULL - ); + `The server is at capacity: ${pool.numPendingAcquires()} exports are already waiting for a worker (limit is ${queueLimit}). Please retry shortly.` + ).setCode(errorCodes.QUEUE_FULL); } // Acquire the worker along with the id of resource and work count @@ -441,9 +440,10 @@ export const postWork = async (chart, options) => { (options.payload?.requestId ? `For request with ID ${options.payload?.requestId} - ` : '') + - `Error encountered when acquiring an available entry: ${acquireCounter()}ms.`, - errorCodes.ACQUIRE_TIMEOUT - ).setError(error); + `Error encountered when acquiring an available entry: ${acquireCounter()}ms.` + ) + .setCode(errorCodes.ACQUIRE_TIMEOUT) + .setError(error); } log(4, '[pool] Acquired a worker handle.'); @@ -484,16 +484,18 @@ export const postWork = async (chart, options) => { result.message === 'Rasterization timeout' ) { throw new ExportError( - 'Rasterization timeout: your chart may be too complex or large, and failed to render within the allotted time.', - errorCodes.RASTERIZATION_TIMEOUT - ).setError(result); + 'Rasterization timeout: your chart may be too complex or large, and failed to render within the allotted time.' + ) + .setCode(errorCodes.RASTERIZATION_TIMEOUT) + .setError(result); } else { throw new ExportError( (options.payload?.requestId ? `For request with ID ${options.payload?.requestId} - ` - : '') + `Error encountered during export: ${exportCounter()}ms.`, - errorCodes.EXPORT_FAILED - ).setError(result); + : '') + `Error encountered during export: ${exportCounter()}ms.` + ) + .setCode(errorCodes.EXPORT_FAILED) + .setError(result); } } diff --git a/lib/server/error.js b/lib/server/error.js index dbe22844..a0f0f236 100644 --- a/lib/server/error.js +++ b/lib/server/error.js @@ -31,9 +31,40 @@ const logErrorMiddleware = (error, req, res, next) => { * @param {Function} next - The next middleware function. */ const returnErrorMiddleware = (error, req, res, next) => { + // NOTE: Once the response has started there is no status left to set, and + // handing the error onwards would let Express's default handler take + // over, which answers 500. Ending the response is the only action here + // that cannot produce one. + if (res.headersSent) { + return res.end(); + } + // Gather all requied information for the response const { statusCode: stCode, status, message, stack, errorCode } = error; - const statusCode = stCode || status || 400; + let statusCode = stCode || status || 400; + + // NOTE: This server must never answer with a 5xx. Treat that as an absolute + // rule, not a preference. + // + // Nothing in this codebase sets a 5xx deliberately, but the status is not + // always ours: setError copies statusCode up from a wrapped error, and + // wrapped errors include ones from outbound HTTP calls, which can carry + // any status a remote gave us. Clamping at the one place every error + // response passes through makes a 5xx structurally impossible instead of + // something to be careful about. + // + // It is logged loudly because reaching here means an error carried a + // status it should not have, which is worth knowing about even though the + // response is safe. + if (statusCode >= 500 || statusCode < 100) { + logWithStack( + 1, + error, + `[server] An error carried the out-of-contract status ${statusCode}, answering with 400 instead. This server must never return a 5xx.` + ); + + statusCode = 400; + } // Set and return response // diff --git a/tests/unit/server_error.test.js b/tests/unit/server_error.test.js new file mode 100644 index 00000000..cd3cbe0b --- /dev/null +++ b/tests/unit/server_error.test.js @@ -0,0 +1,132 @@ +import errorHandler from '../../lib/server/error.js'; +import { setLogLevel } from '../../lib/logger.js'; + +// Keep the suite quiet - the clamp below logs at error level by design +setLogLevel(0); + +// The module registers its middlewares through app.use rather than exporting +// them, so collect them from a stand-in app. The last one registered is the +// middleware that writes the response. +const registered = []; +errorHandler({ use: (middleware) => registered.push(middleware) }); +const returnError = registered[registered.length - 1]; + +/** + * Builds a minimal stand-in for an Express response that records what was done + * to it. + * + * @param {Object} options - Options for the stand-in. + * @param {boolean} options.headersSent - Whether the response has already begun. + * + * @returns {Object} The stand-in response. + */ +const makeResponse = ({ headersSent = false } = {}) => ({ + headersSent, + statusCode: null, + body: null, + ended: false, + status(code) { + this.statusCode = code; + return this; + }, + json(payload) { + this.body = payload; + return this; + }, + end() { + this.ended = true; + return this; + } +}); + +describe('server error middleware', () => { + it('defaults to 400 when the error carries no status', () => { + const response = makeResponse(); + returnError(new Error('no status here'), {}, response, () => {}); + + expect(response.statusCode).toBe(400); + expect(response.body.statusCode).toBe(400); + }); + + it('preserves a 4xx status', () => { + const response = makeResponse(); + const error = new Error('unauthorized'); + error.statusCode = 401; + + returnError(error, {}, response, () => {}); + + expect(response.statusCode).toBe(401); + }); + + // This server must never answer with a 5xx. The status is not always ours: + // errors are wrapped as they travel up the stack, and a wrapped error from an + // outbound HTTP call can carry any status a remote gave us. + it.each([500, 502, 503, 504, 599])( + 'clamps the out-of-contract status %i to 400', + (status) => { + const response = makeResponse(); + const error = new Error('should not escape as a 5xx'); + error.statusCode = status; + + returnError(error, {}, response, () => {}); + + expect(response.statusCode).toBe(400); + expect(response.body.statusCode).toBe(400); + } + ); + + it('clamps a status below the valid range to 400', () => { + const response = makeResponse(); + const error = new Error('nonsense status'); + error.statusCode = 12; + + returnError(error, {}, response, () => {}); + + expect(response.statusCode).toBe(400); + }); + + it('never answers with a 5xx for any status an error might carry', () => { + for (let status = 100; status <= 599; status++) { + const response = makeResponse(); + const error = new Error('sweep'); + error.statusCode = status; + + returnError(error, {}, response, () => {}); + + expect(response.statusCode).toBeLessThan(500); + expect(response.body.statusCode).toBeLessThan(500); + } + }); + + it('includes the errorCode when the error carries one', () => { + const response = makeResponse(); + const error = new Error('at capacity'); + error.statusCode = 400; + error.errorCode = 'EXPORT_QUEUE_FULL'; + + returnError(error, {}, response, () => {}); + + expect(response.body.errorCode).toBe('EXPORT_QUEUE_FULL'); + }); + + it('omits the errorCode entirely when the error carries none', () => { + const response = makeResponse(); + returnError(new Error('plain'), {}, response, () => {}); + + expect('errorCode' in response.body).toBe(false); + }); + + // Handing the error onwards once the response has begun would let Express's + // own handler take over, which answers 500. + it('ends the response without touching the status when headers are sent', () => { + const response = makeResponse({ headersSent: true }); + const error = new Error('too late to set a status'); + error.statusCode = 500; + + returnError(error, {}, response, () => {}); + + expect(response.ended).toBe(true); + expect(response.statusCode).toBeNull(); + expect(response.body).toBeNull(); + }); +}); From 4d861341298b9fd0f8bde6f21b4e957824e1ac2e Mon Sep 17 00:00:00 2001 From: cvasseng Date: Wed, 29 Jul 2026 13:20:27 +0200 Subject: [PATCH 08/34] Discard exports whose client has already disconnected Three changes that only make sense together: without correct detection there is nothing to propagate, and without propagation detection only suppresses a response. Detection was wrong. The socket close listener set its flag only when the close carried an error, so a clean disconnect - a proxy idle timeout, or a caller cancelling - was never detected. It now listens on the response and uses writableFinished to tell a client leaving early from the normal close after a completed response. That also lets the removeAllListeners('close') call go, which stripped Node's and Express's own socket listeners along with ours. The signal is now carried to the pool, which drops the work before it takes a queue slot or a worker. Note the honest limit: an export already being rendered when its client leaves still runs to completion, because the browser operations cannot be cancelled. The win is entirely in not starting work for someone who has gone. Abandoned work is counted separately rather than as a failure, and excluded from the success ratio. Without that, a server whose callers were timing out reported itself as failing when nothing had gone wrong on its side - measured, the ratio read 49% on a healthy server during an abandon storm, and now reads 100%. Measured, 60 clients all abandoning after 150ms over 8s, against the bounded queue from the previous commit: pool when the load stopped used=8 pending=7 -> used=0 pending=0 exports finished after the last client had left 14 -> 0 discarded before taking a worker 0 -> 848 Checked for the feedback effect that made the first queue-limit attempt worse: request count was unchanged (3232 -> 3221), so a cheaper discard path did not increase the load it has to absorb. No regression on the normal path, which now runs this listener on every request: concurrency 4 gives 18.68 req/s at p50 201ms against an 18.95 req/s baseline, the concurrency 150 gate passes, page isolation passes, and droppedExports stays 0 throughout. Worth noting the queue limit had already removed most of this problem - the unbounded queue left 1367 exports queued and took 5s to drain, versus 7 and 2s here. This closes the remainder. --- CHANGELOG.md | 5 ++++ lib/errors/codes.js | 6 +++++ lib/pool.js | 46 +++++++++++++++++++++++++++++++++-- lib/server/routes/export.js | 48 ++++++++++++++++++++++++------------- lib/server/routes/health.js | 35 +++++++++++++++++++++------ 5 files changed, 115 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 224b8e3a..58827d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,17 @@ _Fixes:_ - Fixed an issue where the server never recovered if the browser process died, for example when killed by an out of memory reaper. The browser was launched once at startup and the guard preventing a second launch could never be cleared, so every export from that point failed while the pool continued to report healthy workers. The browser is now relaunched when it is found to be missing, and the workers holding pages from the dead browser are recognised as stale and replaced. This is detected by tracking which browser a worker was created against, because a page belonging to a browser that no longer exists still reports itself as open and so cannot be asked whether it is usable. - Ensured that a worker's page has finished being cleared before the worker is handed to the next export. The clearing was previously started when the worker was released but never awaited, so it overlapped the following export whenever one was already waiting for a worker. A page that cannot be cleared now recycles its worker instead of being exported onto. - Fixed a resource leak where a browser page was left open if configuring it failed, for example when injecting the Highcharts scripts did not succeed. As the pool retries worker creation on an interval, a sustained failure leaked a browser page on every attempt until the browser ran out of memory. +- Fixed an issue where an export whose client had already disconnected still occupied a place in the queue and then a worker, producing a chart that nobody would receive. Such work is now discarded before it takes a worker. Note that an export already being rendered when its client leaves still runs to completion, as the underlying browser operations cannot be cancelled. +- Fixed the detection of a client disconnecting. Only a socket closing with an error was treated as an abandoned request, so a clean disconnect, such as a proxy idle timeout or a caller cancelling, was not detected at all. +- Removed a call that stripped every `close` listener from the request socket, including those belonging to Node.js and Express. +- Fixed the reported export success ratio, which counted exports abandoned by their client as failures. A server whose callers were timing out therefore reported itself as failing when nothing had gone wrong on its side. Abandoned exports are now excluded from the calculation, and from the moving average. - Ensured error responses can never carry a status outside the 1xx to 4xx range. The status of an error is not always set locally, as it is carried up from wrapped errors, and those include errors from outbound HTTP requests which can hold any status a remote returned. Anything outside the range is now answered as 400 and logged. Additionally, an error raised after the response has already begun now ends the response instead of being passed on, which previously allowed the framework's own handler to answer in its place. _New Features:_ - Added the `POOL_QUEUE_LIMIT`/`--queueLimit`/`queueLimit` option, capping how many exports may wait for a worker, and defaulting to four times `maxWorkers`. Requests arriving beyond the limit are refused before their body is parsed, so a refused request costs almost nothing. The rationale is that export throughput does not improve past the pool size, so queueing beyond it adds latency and memory use without adding capacity. Raise it to accept deeper queues at the cost of higher latency under load. - Added the `POOL_QUEUE_REJECT_DELAY`/`--queueRejectDelay`/`queueRejectDelay` option, defaulting to 500 milliseconds, which is how long the server waits before answering a request it is refusing for capacity. This is deliberate backpressure. Answering instantly lets clients that retry immediately raise the request rate by orders of magnitude, at which point the server spends its whole event loop refusing requests and starves the exports already in progress. The acquire timeout used to provide this throttling as a side effect of making clients wait; bounding the queue removes that, so the delay restores it explicitly and far more cheaply, holding only a socket rather than a parsed body and a queue slot. Set it to 0 to answer immediately, which is only advisable when something upstream is limiting the request rate. +- Added `abandonedExports` and `rejectedForCapacity` counters to the `/health` response, reporting exports discarded because their client disconnected and requests refused because the queue was full. Both were previously indistinguishable from ordinary failures. Existing properties are unchanged. - Added an `errorCode` property to error responses, so that a request refused because the server was busy can be told apart from one refused because it was malformed. Both are reported with the same status code, which previously left the message text as the only way to distinguish them. The codes are `EXPORT_INVALID_REQUEST`, `EXPORT_QUEUE_FULL`, `EXPORT_ACQUIRE_TIMEOUT`, `EXPORT_RASTERIZATION_TIMEOUT` and `EXPORT_FAILED`, and may be relied upon by callers. Status codes and the rest of the response body are unchanged, and the property is absent on errors that carry no code. # 5.1.0 diff --git a/lib/errors/codes.js b/lib/errors/codes.js index c6929187..aa62c8ca 100644 --- a/lib/errors/codes.js +++ b/lib/errors/codes.js @@ -42,6 +42,12 @@ export const errorCodes = { // The chart was too large or complex to render within the allotted time. RASTERIZATION_TIMEOUT: 'EXPORT_RASTERIZATION_TIMEOUT', + // The client disconnected before the export could be served, so the work was + // discarded. Never reaches a caller by definition - it exists so that + // abandoned work is distinguishable in logs and counters from work that + // genuinely failed. + CLIENT_GONE: 'EXPORT_CLIENT_GONE', + // The export failed for a reason that is not one of the above. EXPORT_FAILED: 'EXPORT_FAILED' }; diff --git a/lib/pool.js b/lib/pool.js index fe1dab69..e7eeee27 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -40,7 +40,8 @@ export const stats = { timeSpent: 0, droppedExports: 0, spentAverage: 0, - rejectedForCapacity: 0 + rejectedForCapacity: 0, + abandonedExports: 0 }; let poolConfig = {}; @@ -419,6 +420,27 @@ export const postWork = async (chart, options) => { ).setCode(errorCodes.QUEUE_FULL); } + // NOTE: Drop work whose client has already gone, before it takes a queue + // slot or a worker. + // + // Measured on the unbounded queue: 60 clients that each gave up after + // 150ms left 1367 exports still queued a second after the last client + // had disconnected, and the pool spent a further five seconds + // rendering charts nobody would receive. Under a retry storm that + // compounds - every retry adds work while the abandoned original is + // still being rendered - so capacity falls with each round. + const abortSignal = options.payload?.abortSignal; + + if (abortSignal?.aborted) { + ++stats.abandonedExports; + + throw new ExportError( + (options.payload?.requestId + ? `For request with ID ${options.payload?.requestId} - ` + : '') + 'The client disconnected before a worker was available.' + ).setCode(errorCodes.CLIENT_GONE); + } + // Acquire the worker along with the id of resource and work count const acquireCounter = measureTime(); try { @@ -447,6 +469,20 @@ export const postWork = async (chart, options) => { } log(4, '[pool] Acquired a worker handle.'); + // The client may have gone while this request was queued. Hand the worker + // straight back rather than spending it on a result nobody will read. + if (abortSignal?.aborted) { + ++stats.abandonedExports; + pool.release(workerHandle); + workerHandle = null; + + throw new ExportError( + (options.payload?.requestId + ? `For request with ID ${options.payload?.requestId} - ` + : '') + 'The client disconnected while waiting for a worker.' + ).setCode(errorCodes.CLIENT_GONE); + } + if (!workerHandle.page) { throw new ExportError( 'Resolved worker page is invalid: the pool setup is wonky.' @@ -528,7 +564,13 @@ export const postWork = async (chart, options) => { options }; } catch (error) { - ++stats.droppedExports; + // NOTE: Work discarded because its client left is not a failure of the + // server, so it is counted separately rather than inflating + // droppedExports and depressing the success ratio that /health + // reports. It has its own counter, incremented where it is detected. + if (error.errorCode !== errorCodes.CLIENT_GONE) { + ++stats.droppedExports; + } if (workerHandle) { pool.release(workerHandle); diff --git a/lib/server/routes/export.js b/lib/server/routes/export.js index 6e850da0..23b05b86 100644 --- a/lib/server/routes/export.js +++ b/lib/server/routes/export.js @@ -160,12 +160,25 @@ const exportHandler = async (request, response, next) => { return response.send(callResponse); } - let connectionAborted = false; + // NOTE: Notice the client going away, so that work nobody is waiting for can + // be dropped rather than occupying the queue and then a worker. + // + // This listens on the response rather than the socket, and does not + // require the close to have carried an error. The previous check only + // set the flag when the socket closed with `hadErrors`, so a clean + // disconnect - a proxy idle timeout, or a caller cancelling - was not + // detected at all. writableFinished distinguishes the client leaving + // early from the normal close after a completed response. + const abortController = new AbortController(); + + response.once('close', () => { + if (!response.writableFinished) { + abortController.abort(); - // In case the connection is closed, force to abort further actions - request.socket.on('close', (hadErrors) => { - if (hadErrors) { - connectionAborted = true; + log( + 4, + `[export] The client closed the connection for request ${uniqueId} before it was answered.` + ); } }); @@ -213,7 +226,10 @@ const exportHandler = async (request, response, next) => { svg: body.svg || false, b64: body.b64 || false, noDownload: body.noDownload || false, - requestId: uniqueId + requestId: uniqueId, + // Lets the pool drop this export if the client gives up before a worker + // becomes free + abortSignal: abortController.signal }; // Test xlink:href elements from payload's SVG @@ -227,8 +243,16 @@ const exportHandler = async (request, response, next) => { // Start the export process await startExport(options, (error, info) => { - // Remove the close event from the socket - request.socket.removeAllListeners('close'); + // NOTE: Check this before looking at the error. An abandoned export is + // reported as an error by design, and there is nobody left to answer, + // so raising it here would only produce a failed write to a closed + // socket and a misleading error in the log. + if (abortController.signal.aborted) { + return log( + 4, + `[export] Discarding the result for request ${uniqueId}: the client is gone.` + ); + } // After the whole exporting process if (defaultOptions.server.benchmarking) { @@ -238,14 +262,6 @@ const exportHandler = async (request, response, next) => { ); } - // If the connection was closed, do nothing - if (connectionAborted) { - return log( - 3, - `[export] The client closed the connection before the chart finished processing.` - ); - } - // If error, log it and send it to the error middleware if (error) { throw error; diff --git a/lib/server/routes/health.js b/lib/server/routes/health.js index 5925cc68..a1bccbdf 100644 --- a/lib/server/routes/health.js +++ b/lib/server/routes/health.js @@ -48,18 +48,37 @@ function calculateMovingAverage() { */ export const startSuccessRate = () => setInterval(() => { - const stats = pool.getStats(); - const successRatio = - stats.exportAttempts === 0 - ? 1 - : (stats.performedExports / stats.exportAttempts) * 100; + const successRatio = calculateSuccessRatio(pool.getStats()); - successRates.push(successRatio); + successRates.push(successRatio === null ? 1 : successRatio); if (successRates.length > windowSize) { successRates.shift(); } }, recordInterval); +/** + * Calculates the ratio of exports that succeeded, as a percentage. + * + * Exports abandoned by their client are excluded from the denominator. They are + * not failures of the server - the caller went away - and counting them would + * report a healthy server as failing whenever callers time out or cancel, which + * is exactly when this figure is most likely to be looked at. + * + * @param {Object} stats - The pool statistics. + * + * @returns {number|null} The success ratio as a percentage, or null when no + * export has been attempted yet. + */ +function calculateSuccessRatio(stats) { + const attempts = stats.exportAttempts - stats.abandonedExports; + + if (attempts <= 0) { + return null; + } + + return (stats.performedExports / attempts) * 100; +} + /** * Adds the /health and /success-moving-average routes * which output basic stats for the server. @@ -92,8 +111,10 @@ export default function addHealthRoutes(app) { averageProcessingTime: stats.spentAverage, performedExports: stats.performedExports, failedExports: stats.droppedExports, + abandonedExports: stats.abandonedExports, + rejectedForCapacity: stats.rejectedForCapacity, exportAttempts: stats.exportAttempts, - sucessRatio: (stats.performedExports / stats.exportAttempts) * 100, + sucessRatio: calculateSuccessRatio(stats), // eslint-disable-next-line import/no-named-as-default-member pool: pool.getPoolInfoJSON(), From 4f5e4462f0561b7dc161a44846c388da17831302 Mon Sep 17 00:00:00 2001 From: cvasseng Date: Wed, 29 Jul 2026 13:46:30 +0200 Subject: [PATCH 09/34] Confirm the browser process has exited, and report browser health Closing a browser does not guarantee its process has gone. Observed directly: a process survived a close that resolved without error, kept running as a child of the server, and held Chrome's lock on the user data directory - which every launch here shares - so no later browser could start at all. The launch options deliberately leave SIGINT, SIGTERM and SIGHUP unhandled, so nothing cleans up on our behalf either. The close is now followed by confirming the process has exited, killing it if it has not. Liveness is checked with signal 0 rather than the child process object's exitCode, because that reads as undefined in some cases and `undefined !== null` would have been taken as exited - silently skipping both the wait and the kill, which is exactly the bug this is meant to catch. Adds browserConnected and consecutiveCreateFailures to /health. These distinguish a server that has lost its browser from one that has a browser but cannot make pages with it, which previously looked identical from outside and cost real time to tell apart while investigating. Deliberately not included: a circuit breaker that replaced the browser after repeated worker creation failures. Two reasons. The premise was wrong. It assumed tarn's createRetryInterval drives retrying at five attempts a second indefinitely. Measured under sustained load with page setup failing, creation was attempted 20 times in 15 seconds, about 1.3/s, because attempts are bounded by demand and by acquireTimeout rather than by the retry interval. An A/B with a growing backoff produced 20 attempts either way - the delays were applied, and the count did not move. There is no storm to cap. It also made things worse. Replacing a browser means closing and relaunching against the same user data directory, and the replacement loses a race with the outgoing process for that lock. Two attempts at sequencing it left the pool unable to start any browser at all, and the browser was never the fault in the first place: only page setup was failing. Where the browser genuinely dies, the generation check added earlier already handles it. Verified: normal load unchanged at 18.86 req/s and p50 200ms, the browser recovery test still passes, and a SIGTERM under load now leaves no browser process behind. --- CHANGELOG.md | 2 + lib/browser.js | 121 +++++++++++++++++++++++++++++++++++- lib/pool.js | 17 +++++ lib/server/routes/health.js | 5 +- 4 files changed, 141 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58827d0e..1bb3e6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,14 @@ _Fixes:_ - Fixed the detection of a client disconnecting. Only a socket closing with an error was treated as an abandoned request, so a clean disconnect, such as a proxy idle timeout or a caller cancelling, was not detected at all. - Removed a call that stripped every `close` listener from the request socket, including those belonging to Node.js and Express. - Fixed the reported export success ratio, which counted exports abandoned by their client as failures. A server whose callers were timing out therefore reported itself as failing when nothing had gone wrong on its side. Abandoned exports are now excluded from the calculation, and from the moving average. +- Fixed the browser process being able to outlive the call that closes it. Closing does not guarantee that the process has ended, and the launch options deliberately leave process signals unhandled, so nothing cleaned up afterwards. A surviving process keeps the lock Chrome holds on its user data directory, which then prevents any later browser from starting at all. The process is now confirmed to have exited, and killed if it has not. - Ensured error responses can never carry a status outside the 1xx to 4xx range. The status of an error is not always set locally, as it is carried up from wrapped errors, and those include errors from outbound HTTP requests which can hold any status a remote returned. Anything outside the range is now answered as 400 and logged. Additionally, an error raised after the response has already begun now ends the response instead of being passed on, which previously allowed the framework's own handler to answer in its place. _New Features:_ - Added the `POOL_QUEUE_LIMIT`/`--queueLimit`/`queueLimit` option, capping how many exports may wait for a worker, and defaulting to four times `maxWorkers`. Requests arriving beyond the limit are refused before their body is parsed, so a refused request costs almost nothing. The rationale is that export throughput does not improve past the pool size, so queueing beyond it adds latency and memory use without adding capacity. Raise it to accept deeper queues at the cost of higher latency under load. - Added the `POOL_QUEUE_REJECT_DELAY`/`--queueRejectDelay`/`queueRejectDelay` option, defaulting to 500 milliseconds, which is how long the server waits before answering a request it is refusing for capacity. This is deliberate backpressure. Answering instantly lets clients that retry immediately raise the request rate by orders of magnitude, at which point the server spends its whole event loop refusing requests and starves the exports already in progress. The acquire timeout used to provide this throttling as a side effect of making clients wait; bounding the queue removes that, so the delay restores it explicitly and far more cheaply, holding only a socket rather than a parsed body and a queue slot. Set it to 0 to answer immediately, which is only advisable when something upstream is limiting the request rate. +- Added `browserConnected` and `consecutiveCreateFailures` to the `/health` response, reporting whether a browser is currently available and how many worker creations have failed in a row. Between them these distinguish a server that has lost its browser from one that has a browser but cannot make pages with it, which previously looked the same from outside. Existing properties are unchanged. - Added `abandonedExports` and `rejectedForCapacity` counters to the `/health` response, reporting exports discarded because their client disconnected and requests refused because the queue was full. Both were previously indistinguishable from ordinary failures. Existing properties are unchanged. - Added an `errorCode` property to error responses, so that a request refused because the server was busy can be told apart from one refused because it was malformed. Both are reported with the same status code, which previously left the message text as the only way to distinguish them. The codes are `EXPORT_INVALID_REQUEST`, `EXPORT_QUEUE_FULL`, `EXPORT_ACQUIRE_TIMEOUT`, `EXPORT_RASTERIZATION_TIMEOUT` and `EXPORT_FAILED`, and may be relied upon by callers. Status codes and the rest of the response body are unchanged, and the property is absent on errors that carry no code. diff --git a/lib/browser.js b/lib/browser.js index ca13db30..5c656cf4 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -110,6 +110,120 @@ function handleDisconnect() { ); } +/** + * Reports whether a process is still running, by asking the operating system + * rather than trusting the child process object. + * + * NOTE: exitCode and signalCode are not dependable here. Both are null for a + * running process, but either can be undefined depending on how the + * process object was produced, and `undefined !== null` reads as exited - + * which silently skips both the wait and the kill below, leaving the + * process alive. Signal 0 performs the permission and existence checks + * without delivering anything. + * + * @param {Object} proc - The child process to check. + * + * @returns {boolean} True while the process still exists. + */ +function isAlive(proc) { + if (!proc?.pid) { + return false; + } + + try { + process.kill(proc.pid, 0); + return true; + } catch (error) { + // ESRCH means no such process; EPERM means it exists but is not ours + return error.code === 'EPERM'; + } +} + +/** + * Waits for a child process to exit, up to a limit. + * + * @param {Object} proc - The child process to wait for. + * @param {number} timeout - How long to wait, in milliseconds. + * + * @returns {Promise} True if the process exited within the limit. + */ +function waitForExit(proc, timeout) { + return new Promise((resolve) => { + if (!isAlive(proc)) { + return resolve(true); + } + + const onExit = () => { + clearTimeout(timer); + resolve(true); + }; + + const timer = setTimeout(() => { + proc.removeListener('exit', onExit); + resolve(false); + }, timeout); + + proc.once('exit', onExit); + }); +} + +/** + * Closes a browser and makes certain its process has actually gone. + * + * @param {Object} instance - The Puppeteer browser instance to end. + * + * @returns {Promise} Resolves once the process has exited, or once giving + * up waiting for it. + */ +async function terminate(instance) { + // Take the process reference before closing, as it is not reachable afterwards + const proc = instance.process(); + + try { + await instance.close(); + } catch (error) { + logWithStack(2, error, '[browser] Could not cleanly close the browser.'); + } + + if (!proc) { + log( + 2, + '[browser] No process handle for the browser, so its exit cannot be confirmed.' + ); + return; + } + + // NOTE: close() resolving does not mean the process has gone. It can resolve + // when the connection drops, and the launch options deliberately disable + // Puppeteer's signal handling, so nothing will clean up on our behalf. + // + // A surviving process keeps Chrome's lock on the user data directory, + // which is shared by every launch here, so leaving one behind stops any + // later browser from starting at all - and on shutdown it leaks a browser + // process outright. + if (await waitForExit(proc, 2000)) { + return; + } + + log( + 2, + '[browser] The browser process is still running after being closed, killing it.' + ); + + try { + proc.kill('SIGKILL'); + } catch (error) { + logWithStack(2, error, '[browser] Could not kill the browser process.'); + } + + if (isAlive(proc)) { + log( + 1, + `[browser] The browser process ${proc.pid} has still not exited after being killed.` + ); + } +} + /** * Creates a Puppeteer browser instance with the specified arguments. * @@ -252,9 +366,10 @@ export async function close() { // crash and does not trigger a relaunch closingOnPurpose = true; - // Close the browser when connnected - if (browser?.connected) { - await browser.close(); + // Close the browser and make sure its process has gone, so that shutdown does + // not leave one behind holding the user data directory + if (browser) { + await terminate(browser); } browser = undefined; diff --git a/lib/pool.js b/lib/pool.js index e7eeee27..d6a8188a 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -49,6 +49,17 @@ let poolConfig = {}; // The resolved maximum number of exports allowed to wait for a worker let queueLimit = 0; +// Worker creations that have failed in a row, reset by any success. Used to stop +// the pool retrying against a browser that is not going to start working again. +let consecutiveCreateFailures = 0; + +/** + * Returns the number of worker creations that have failed in a row. + * + * @returns {number} The current consecutive failure count. + */ +export const getConsecutiveCreateFailures = () => consecutiveCreateFailures; + /** * Resolves the queue limit from the pool configuration. * @@ -112,6 +123,8 @@ const factory = { throw new ExportError('The page is invalid or closed.'); } + consecutiveCreateFailures = 0; + log( 3, `[pool] Successfully created a worker ${id} - took ${ @@ -119,6 +132,8 @@ const factory = { } ms.` ); } catch (error) { + ++consecutiveCreateFailures; + throw new ExportError( 'Error encountered when creating a new page.' ).setError(error); @@ -629,5 +644,7 @@ export default { getPool, getPoolInfo, getPoolInfoJSON, + getQueueLimit, + getConsecutiveCreateFailures, getStats: () => stats }; diff --git a/lib/server/routes/health.js b/lib/server/routes/health.js index a1bccbdf..4ab277fc 100644 --- a/lib/server/routes/health.js +++ b/lib/server/routes/health.js @@ -18,7 +18,8 @@ import { log } from '../../logger.js'; import { version } from '../../cache.js'; import { addInterval } from '../../intervals.js'; -import pool from '../../pool.js'; +import pool, { getConsecutiveCreateFailures } from '../../pool.js'; +import { isConnected as browserIsConnected } from '../../browser.js'; import { __dirname } from '../../utils.js'; const pkgFile = JSON.parse(readFileSync(pather(__dirname, 'package.json'))); @@ -113,6 +114,8 @@ export default function addHealthRoutes(app) { failedExports: stats.droppedExports, abandonedExports: stats.abandonedExports, rejectedForCapacity: stats.rejectedForCapacity, + consecutiveCreateFailures: getConsecutiveCreateFailures(), + browserConnected: browserIsConnected(), exportAttempts: stats.exportAttempts, sucessRatio: calculateSuccessRatio(stats), // eslint-disable-next-line import/no-named-as-default-member From f6342e337d08ff2f18e532702a6199b57a52cceb Mon Sep 17 00:00:00 2001 From: cvasseng Date: Wed, 29 Jul 2026 13:50:06 +0200 Subject: [PATCH 10/34] Reuse the DOM and purifier when sanitizing SVGs A JSDOM window and a DOMPurify instance were built on every call, which means on every SVG export. That is the bulk of the work sanitizing does, and it is synchronous, so the time went on the event loop and delayed every other request in flight rather than only the one being sanitized. Only the instance is shared. The options stay per call, because FORBID_ATTR depends on configuration that can be read at any time and DOMPurify applies whatever options each call passes. Measured 2.84ms per call before, 0.27ms after. The existing sanitize unit tests pass unchanged, and a check that the same input still sanitizes identically after 50 intervening calls confirms nothing carries over between them. --- CHANGELOG.md | 4 ++++ lib/sanitize.js | 30 +++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bb3e6fa..b129bbb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ _New Features:_ - Added `abandonedExports` and `rejectedForCapacity` counters to the `/health` response, reporting exports discarded because their client disconnected and requests refused because the queue was full. Both were previously indistinguishable from ordinary failures. Existing properties are unchanged. - Added an `errorCode` property to error responses, so that a request refused because the server was busy can be told apart from one refused because it was malformed. Both are reported with the same status code, which previously left the message text as the only way to distinguish them. The codes are `EXPORT_INVALID_REQUEST`, `EXPORT_QUEUE_FULL`, `EXPORT_ACQUIRE_TIMEOUT`, `EXPORT_RASTERIZATION_TIMEOUT` and `EXPORT_FAILED`, and may be relied upon by callers. Status codes and the rest of the response body are unchanged, and the property is absent on errors that carry no code. +_Enhancements:_ + +- Reduced the cost of sanitizing incoming SVGs by around ten times, from 2.84ms to 0.27ms per call, by reusing the DOM and purifier between requests rather than building them on every export. As sanitizing is synchronous, that time was spent blocking the event loop, so it delayed every other request in flight rather than only the one being sanitized. + # 5.1.0 _New Features:_ diff --git a/lib/sanitize.js b/lib/sanitize.js index dd95095a..e9df8a68 100644 --- a/lib/sanitize.js +++ b/lib/sanitize.js @@ -21,6 +21,32 @@ import { JSDOM } from 'jsdom'; import DOMPurify from 'dompurify'; import { envs } from './envs.js'; + +// The purifier, built on first use and then reused. +// +// NOTE: Building a DOM and a purifier per call is by far the most expensive part +// of sanitizing, and this runs on every SVG export. It is also synchronous, +// so the cost is paid on the event loop and delays every other request in +// flight, not just this one. +// +// Only the instance is shared. The options stay per call, since FORBID_ATTR +// depends on configuration that can be read at any time, and DOMPurify +// applies the options it is given on each call. +let purifier; + +/** + * Returns the shared purifier, building it if this is the first call. + * + * @returns {Object} The DOMPurify instance. + */ +function getPurifier() { + if (!purifier) { + purifier = DOMPurify(new JSDOM('').window); + } + + return purifier; +} + /** * Sanitizes a given HTML string by removing tags and any content within them.\r\n *\r\n * @param {string} input The HTML string to be sanitized.\r\n * @returns {string} The sanitized HTML string.\r\n */\r\nexport function sanitize(input) {\r\n const forbidden = [];\r\n\r\n if (!envs.OTHER_ALLOW_XLINK) {\r\n forbidden.push('xlink:href');\r\n }\r\n\r\n const window = new JSDOM('').window;\r\n const purify = DOMPurify(window);\r\n return purify.sanitize(input, {\r\n ADD_TAGS: ['foreignObject'],\r\n FORBID_ATTR: forbidden\r\n });\r\n}\r\n\r\nexport default sanitize;\r\n","/*******************************************************************************\r\n\r\nHighcharts Export Server\r\n\r\nCopyright (c) 2016-2024, Highsoft\r\n\r\nLicenced under the MIT licence.\r\n\r\nAdditionally a valid Highcharts license is required for use.\r\n\r\nSee LICENSE file in root for details.\r\n\r\n*******************************************************************************/\r\n\r\nimport { log } from './logger.js';\r\n\r\n// Array that contains ids of all ongoing intervals\r\nconst intervalIds = [];\r\n\r\n/**\r\n * Adds id of a setInterval to the intervalIds array.\r\n *\r\n * @param {NodeJS.Timeout} id - Id of an interval.\r\n */\r\nexport const addInterval = (id) => {\r\n intervalIds.push(id);\r\n};\r\n\r\n/**\r\n * Clears all of ongoing intervals by ids gathered in the intervalIds array.\r\n */\r\nexport const clearAllIntervals = () => {\r\n log(4, `[server] Clearing all registered intervals.`);\r\n for (const id of intervalIds) {\r\n clearInterval(id);\r\n }\r\n};\r\n\r\nexport default {\r\n addInterval,\r\n clearAllIntervals\r\n};\r\n","import { envs } from '../envs.js';\r\nimport { logWithStack } from '../logger.js';\r\n\r\n/**\r\n * Middleware for logging errors with stack trace and handling error response.\r\n *\r\n * @param {Error} error - The error object.\r\n * @param {Express.Request} req - The Express request object.\r\n * @param {Express.Response} res - The Express response object.\r\n * @param {Function} next - The next middleware function.\r\n */\r\nconst logErrorMiddleware = (error, req, res, next) => {\r\n // Display the error with stack in a correct format\r\n logWithStack(1, error);\r\n\r\n // Delete the stack for the environment other than the development\r\n if (envs.OTHER_NODE_ENV !== 'development') {\r\n delete error.stack;\r\n }\r\n\r\n // Call the returnErrorMiddleware\r\n next(error);\r\n};\r\n\r\n/**\r\n * Middleware for returning error response.\r\n *\r\n * @param {Error} error - The error object.\r\n * @param {Express.Request} req - The Express request object.\r\n * @param {Express.Response} res - The Express response object.\r\n * @param {Function} next - The next middleware function.\r\n */\r\nconst returnErrorMiddleware = (error, req, res, next) => {\r\n // Gather all requied information for the response\r\n const { statusCode: stCode, status, message, stack } = error;\r\n const statusCode = stCode || status || 400;\r\n\r\n // Set and return response\r\n res.status(statusCode).json({ statusCode, message, stack });\r\n};\r\n\r\nexport default (app) => {\r\n // Add log error middleware\r\n app.use(logErrorMiddleware);\r\n\r\n // Add set status and return error middleware\r\n app.use(returnErrorMiddleware);\r\n};\r\n","/*******************************************************************************\r\n\r\nHighcharts Export Server\r\n\r\nCopyright (c) 2016-2024, Highsoft\r\n\r\nLicenced under the MIT licence.\r\n\r\nAdditionally a valid Highcharts license is required for use.\r\n\r\nSee LICENSE file in root for details.\r\n\r\n*******************************************************************************/\r\n\r\nimport rateLimit from 'express-rate-limit';\r\n\r\nimport { log } from '../logger.js';\r\n\r\n/**\r\n * Middleware for enabling rate limiting on the specified Express app.\r\n *\r\n * @param {Express} app - The Express app instance.\r\n * @param {Object} limitConfig - Configuration options for rate limiting.\r\n */\r\nexport default (app, limitConfig) => {\r\n const msg =\r\n 'Too many requests, you have been rate limited. Please try again later.';\r\n\r\n // Options for the rate limiter\r\n const rateOptions = {\r\n max: limitConfig.maxRequests || 30,\r\n window: limitConfig.window || 1,\r\n delay: limitConfig.delay || 0,\r\n trustProxy: limitConfig.trustProxy || false,\r\n skipKey: limitConfig.skipKey || false,\r\n skipToken: limitConfig.skipToken || false\r\n };\r\n\r\n // Set if behind a proxy\r\n if (rateOptions.trustProxy) {\r\n app.enable('trust proxy');\r\n }\r\n\r\n // Create a limiter\r\n const limiter = rateLimit({\r\n windowMs: rateOptions.window * 60 * 1000,\r\n // Limit each IP to 100 requests per windowMs\r\n max: rateOptions.max,\r\n // Disable delaying, full speed until the max limit is reached\r\n delayMs: rateOptions.delay,\r\n handler: (request, response) => {\r\n response.format({\r\n json: () => {\r\n response.status(429).send({ message: msg });\r\n },\r\n default: () => {\r\n response.status(429).send(msg);\r\n }\r\n });\r\n },\r\n skip: (request) => {\r\n // Allow bypassing the limiter if a valid key/token has been sent\r\n if (\r\n rateOptions.skipKey !== false &&\r\n rateOptions.skipToken !== false &&\r\n request.query.key === rateOptions.skipKey &&\r\n request.query.access_token === rateOptions.skipToken\r\n ) {\r\n log(4, '[rate limiting] Skipping rate limiter.');\r\n return true;\r\n }\r\n return false;\r\n }\r\n });\r\n\r\n // Use a limiter as a middleware\r\n app.use(limiter);\r\n\r\n log(\r\n 3,\r\n `[rate limiting] Enabled rate limiting with ${rateOptions.max} requests per ${rateOptions.window} minute for each IP, trusting proxy: ${rateOptions.trustProxy}.`\r\n );\r\n};\r\n","import ExportError from './ExportError.js';\r\n\r\nclass HttpError extends ExportError {\r\n constructor(message, status) {\r\n super(message);\r\n this.status = this.statusCode = status;\r\n }\r\n\r\n setStatus(status) {\r\n this.status = status;\r\n return this;\r\n }\r\n}\r\n\r\nexport default HttpError;\r\n","/*******************************************************************************\r\n\r\nHighcharts Export Server\r\n\r\nCopyright (c) 2016-2024, Highsoft\r\n\r\nLicenced under the MIT licence.\r\n\r\nAdditionally a valid Highcharts license is required for use.\r\n\r\nSee LICENSE file in root for details.\r\n\r\n*******************************************************************************/\r\n\r\nimport { updateVersion, version } from '../../cache.js';\r\nimport { envs } from '../../envs.js';\r\n\r\nimport HttpError from '../../errors/HttpError.js';\r\n\r\n/**\r\n * Adds the POST /change_hc_version/:newVersion route that can be utilized to modify\r\n * the Highcharts version on the server.\r\n *\r\n * TODO: Add auth token and connect to API\r\n */\r\nexport default (app) =>\r\n !app\r\n ? false\r\n : app.post(\r\n '/version/change/:newVersion',\r\n async (request, response, next) => {\r\n try {\r\n const adminToken = envs.HIGHCHARTS_ADMIN_TOKEN;\r\n\r\n // Check the existence of the token\r\n if (!adminToken || !adminToken.length) {\r\n throw new HttpError(\r\n 'The server is not configured to perform run-time version changes: HIGHCHARTS_ADMIN_TOKEN is not set.',\r\n 401\r\n );\r\n }\r\n\r\n // Check if the hc-auth header contain a correct token\r\n const token = request.get('hc-auth');\r\n if (!token || token !== adminToken) {\r\n throw new HttpError(\r\n 'Invalid or missing token: Set the token in the hc-auth header.',\r\n 401\r\n );\r\n }\r\n\r\n // Compare versions\r\n const newVersion = request.params.newVersion;\r\n\r\n // Accept only version strings containing digits, letters, dots, hyphens\r\n if (newVersion && /^[a-zA-Z0-9.-]+$/.test(newVersion)) {\r\n try {\r\n // eslint-disable-next-line import/no-named-as-default-member\r\n await updateVersion(newVersion);\r\n } catch (error) {\r\n throw new HttpError(\r\n `Version change: ${error.message}`,\r\n error.statusCode\r\n ).setError(error);\r\n }\r\n\r\n // Success\r\n response.status(200).send({\r\n statusCode: 200,\r\n version: version(),\r\n message: `Successfully updated Highcharts to version: ${newVersion}.`\r\n });\r\n } else {\r\n // No version specified\r\n throw new HttpError('No new version supplied.', 400);\r\n }\r\n } catch (error) {\r\n next(error);\r\n }\r\n }\r\n );\r\n","/*******************************************************************************\r\n\r\nHighcharts Export Server\r\n\r\nCopyright (c) 2016-2024, Highsoft\r\n\r\nLicenced under the MIT licence.\r\n\r\nAdditionally a valid Highcharts license is required for use.\r\n\r\nSee LICENSE file in root for details.\r\n\r\n*******************************************************************************/\r\n\r\nimport { v4 as uuid } from 'uuid';\r\n\r\nimport { getAllowCodeExecution, startExport } from '../../chart.js';\r\nimport { getOptions, mergeConfigOptions } from '../../config.js';\r\nimport { log } from '../../logger.js';\r\nimport {\r\n fixType,\r\n isCorrectJSON,\r\n isObjectEmpty,\r\n isPrivateRangeUrlFound,\r\n optionsStringify,\r\n measureTime\r\n} from '../../utils.js';\r\n\r\nimport HttpError from '../../errors/HttpError.js';\r\n\r\n// Reversed MIME types\r\nconst reversedMime = {\r\n png: 'image/png',\r\n jpeg: 'image/jpeg',\r\n gif: 'image/gif',\r\n pdf: 'application/pdf',\r\n svg: 'image/svg+xml'\r\n};\r\n\r\n// The requests counter\r\nlet requestsCounter = 0;\r\n\r\n// The array of callbacks to call before a request\r\nconst beforeRequest = [];\r\n\r\n// The array of callbacks to call after a request\r\nconst afterRequest = [];\r\n\r\n/**\r\n * Invokes an array of callback functions with specified parameters, allowing\r\n * customization of request handling.\r\n *\r\n * @param {Function[]} callbacks - An array of callback functions\r\n * to be executed.\r\n * @param {Express.Request} request - The Express request object.\r\n * @param {Express.Response} response - The Express response object.\r\n * @param {Object} data - An object containing parameters like id, uniqueId,\r\n * type, and body.\r\n *\r\n * @returns {boolean} - Returns a boolean indicating the overall result\r\n * of the callback invocations.\r\n */\r\nconst doCallbacks = (callbacks, request, response, data) => {\r\n let result = true;\r\n const { id, uniqueId, type, body } = data;\r\n\r\n callbacks.some((callback) => {\r\n if (callback) {\r\n let callResponse = callback(request, response, id, uniqueId, type, body);\r\n\r\n if (callResponse !== undefined && callResponse !== true) {\r\n result = callResponse;\r\n }\r\n\r\n return true;\r\n }\r\n });\r\n\r\n return result;\r\n};\r\n\r\n/**\r\n * Handles the export requests from the client.\r\n *\r\n * @param {Express.Request} request - The Express request object.\r\n * @param {Express.Response} response - The Express response object.\r\n * @param {Function} next - The next middleware function.\r\n *\r\n * @returns {Promise} - A promise that resolves once the export process\r\n * is complete.\r\n */\r\nconst exportHandler = async (request, response, next) => {\r\n try {\r\n // Start counting time\r\n const stopCounter = measureTime();\r\n\r\n // Create a unique ID for a request\r\n const uniqueId = uuid().replace(/-/g, '');\r\n\r\n // Get the current server's general options\r\n const defaultOptions = getOptions();\r\n\r\n const body = request.body;\r\n const id = ++requestsCounter;\r\n\r\n let type = fixType(body.type);\r\n\r\n // Throw 'Bad Request' if there's no body\r\n if (!body || isObjectEmpty(body)) {\r\n throw new HttpError(\r\n 'The request body is required. Please ensure that your Content-Type header is correct (accepted types are application/json and multipart/form-data).',\r\n 400\r\n );\r\n }\r\n\r\n // All of the below can be used\r\n let instr = isCorrectJSON(body.infile || body.options || body.data);\r\n\r\n // Throw 'Bad Request' if there's no JSON or SVG to export\r\n if (!instr && !body.svg) {\r\n log(\r\n 2,\r\n `The request with ID ${uniqueId} from ${\r\n request.headers['x-forwarded-for'] || request.connection.remoteAddress\r\n } was incorrect:\r\n Content-Type: ${request.headers['content-type']}. \r\n Chart constructor: ${body.constr}.\r\n Dimensions: ${body.width}x${body.height} @ ${body.scale} scale.\r\n Type: ${type}.\r\n Is SVG set? ${typeof body.svg !== 'undefined'}.\r\n B64? ${typeof body.b64 !== 'undefined'}.\r\n No download? ${typeof body.noDownload !== 'undefined'}.\r\n\r\n Payload received: ${JSON.stringify(body.infile || body.options || body.data || body.svg)}\r\n\r\n `\r\n );\r\n\r\n throw new HttpError(\r\n \"No correct chart data found. Ensure that you are using either application/json or multipart/form-data headers. If sending JSON, make sure the chart data is in the 'infile', 'options', or 'data' attribute. If sending SVG, ensure it is in the 'svg' attribute.\",\r\n 400\r\n );\r\n }\r\n\r\n let callResponse = false;\r\n\r\n // Call the before request functions\r\n callResponse = doCallbacks(beforeRequest, request, response, {\r\n id,\r\n uniqueId,\r\n type,\r\n body\r\n });\r\n\r\n // Block the request if one of a callbacks failed\r\n if (callResponse !== true) {\r\n return response.send(callResponse);\r\n }\r\n\r\n let connectionAborted = false;\r\n\r\n // In case the connection is closed, force to abort further actions\r\n request.socket.on('close', (hadErrors) => {\r\n if (hadErrors) {\r\n connectionAborted = true;\r\n }\r\n });\r\n\r\n log(4, `[export] Got an incoming HTTP request with ID ${uniqueId}.`);\r\n\r\n body.constr = (typeof body.constr === 'string' && body.constr) || 'chart';\r\n\r\n // Gather and organize options from the payload\r\n const requestOptions = {\r\n export: {\r\n instr,\r\n type,\r\n constr: body.constr[0].toLowerCase() + body.constr.substr(1),\r\n height: body.height,\r\n width: body.width,\r\n scale: body.scale || defaultOptions.export.scale,\r\n globalOptions: isCorrectJSON(body.globalOptions, true),\r\n themeOptions: isCorrectJSON(body.themeOptions, true)\r\n },\r\n customLogic: {\r\n allowCodeExecution: getAllowCodeExecution(),\r\n allowFileResources: false,\r\n resources: isCorrectJSON(body.resources, true),\r\n callback: body.callback,\r\n customCode: body.customCode\r\n }\r\n };\r\n\r\n if (instr) {\r\n // Stringify JSON with options\r\n requestOptions.export.instr = optionsStringify(\r\n instr,\r\n requestOptions.customLogic.allowCodeExecution\r\n );\r\n }\r\n\r\n // Merge the request options into default ones\r\n const options = mergeConfigOptions(defaultOptions, requestOptions);\r\n\r\n // Save the JSON if exists\r\n options.export.options = instr;\r\n\r\n // Lastly, add the server specific arguments into options as payload\r\n options.payload = {\r\n svg: body.svg || false,\r\n b64: body.b64 || false,\r\n noDownload: body.noDownload || false,\r\n requestId: uniqueId\r\n };\r\n\r\n // Test xlink:href elements from payload's SVG\r\n if (body.svg && isPrivateRangeUrlFound(options.payload.svg)) {\r\n throw new HttpError(\r\n 'SVG potentially contain at least one forbidden URL in xlink:href element. Please review the SVG content and ensure that all referenced URLs comply with security policies.',\r\n 400\r\n );\r\n }\r\n\r\n // Start the export process\r\n await startExport(options, (error, info) => {\r\n // Remove the close event from the socket\r\n request.socket.removeAllListeners('close');\r\n\r\n // After the whole exporting process\r\n if (defaultOptions.server.benchmarking) {\r\n log(\r\n 5,\r\n `[benchmark] Request with ID ${uniqueId} - After the whole exporting process: ${stopCounter()}ms.`\r\n );\r\n }\r\n\r\n // If the connection was closed, do nothing\r\n if (connectionAborted) {\r\n return log(\r\n 3,\r\n `[export] The client closed the connection before the chart finished processing.`\r\n );\r\n }\r\n\r\n // If error, log it and send it to the error middleware\r\n if (error) {\r\n throw error;\r\n }\r\n\r\n // If data is missing, log the message and send it to the error middleware\r\n if (!info || !info.result) {\r\n throw new HttpError(\r\n `Unexpected return from chart generation. Please check your request data. For the request with ID ${uniqueId}, the result is ${info.result}.`,\r\n 400\r\n );\r\n }\r\n\r\n // Get the type from options\r\n type = info.options.export.type;\r\n\r\n // The after request callbacks\r\n doCallbacks(afterRequest, request, response, { id, body: info.result });\r\n\r\n if (info.result) {\r\n // If only base64 is required, return it\r\n if (body.b64) {\r\n // SVG Exception for the Highcharts 11.3.0 version\r\n if (type === 'pdf' || type == 'svg') {\r\n return response.send(\r\n Buffer.from(info.result, 'utf8').toString('base64')\r\n );\r\n }\r\n\r\n return response.send(info.result);\r\n }\r\n\r\n // Set correct content type\r\n response.header('Content-Type', reversedMime[type] || 'image/png');\r\n\r\n // Decide whether to download or not chart file\r\n if (!body.noDownload) {\r\n response.attachment(\r\n `${request.params.filename || request.body.filename || 'chart'}.${\r\n type || 'png'\r\n }`\r\n );\r\n }\r\n\r\n // If SVG, return plain content\r\n return type === 'svg'\r\n ? response.send(info.result)\r\n : response.send(Buffer.from(info.result, 'base64'));\r\n }\r\n });\r\n } catch (error) {\r\n next(error);\r\n }\r\n};\r\n\r\nexport default (app) => {\r\n /**\r\n * Adds the POST / a route for handling POST requests at the root endpoint.\r\n */\r\n app.post('/', exportHandler);\r\n\r\n /**\r\n * Adds the POST /:filename a route for handling POST requests with\r\n * a specified filename parameter.\r\n */\r\n app.post('/:filename', exportHandler);\r\n};\r\n","/*******************************************************************************\r\n\r\nHighcharts Export Server\r\n\r\nCopyright (c) 2016-2024, Highsoft\r\n\r\nLicenced under the MIT licence.\r\n\r\nAdditionally a valid Highcharts license is required for use.\r\n\r\nSee LICENSE file in root for details.\r\n\r\n*******************************************************************************/\r\n\r\nimport { readFileSync } from 'fs';\r\nimport { join as pather } from 'path';\r\nimport { log } from '../../logger.js';\r\n\r\nimport { version } from '../../cache.js';\r\nimport { addInterval } from '../../intervals.js';\r\nimport pool from '../../pool.js';\r\nimport { __dirname } from '../../utils.js';\r\n\r\nconst pkgFile = JSON.parse(readFileSync(pather(__dirname, 'package.json')));\r\n\r\nconst serverStartTime = new Date();\r\n\r\nconst successRates = [];\r\nconst recordInterval = 60 * 1000; // record every minute\r\nconst windowSize = 30; // 30 minutes\r\n\r\n/**\r\n * Calculates moving average indicator based on the data from the successRates\r\n * array.\r\n *\r\n * @returns {number} - A moving average for success ratio of the server exports.\r\n */\r\nfunction calculateMovingAverage() {\r\n const sum = successRates.reduce((a, b) => a + b, 0);\r\n return sum / successRates.length;\r\n}\r\n\r\n/**\r\n * Starts the interval responsible for calculating current success rate ratio\r\n * and gathers\r\n *\r\n * @returns {NodeJS.Timeout} id - Id of an interval.\r\n */\r\nexport const startSuccessRate = () =>\r\n setInterval(() => {\r\n const stats = pool.getStats();\r\n const successRatio =\r\n stats.exportAttempts === 0\r\n ? 1\r\n : (stats.performedExports / stats.exportAttempts) * 100;\r\n\r\n successRates.push(successRatio);\r\n if (successRates.length > windowSize) {\r\n successRates.shift();\r\n }\r\n }, recordInterval);\r\n\r\n/**\r\n * Adds the /health and /success-moving-average routes\r\n * which output basic stats for the server.\r\n */\r\nexport default function addHealthRoutes(app) {\r\n if (!app) {\r\n return false;\r\n }\r\n\r\n // Start processing success rate ratio interval and save its id to the array\r\n // for the graceful clearing on shutdown with injected addInterval funtion\r\n addInterval(startSuccessRate());\r\n\r\n app.get('/health', (_, res) => {\r\n const stats = pool.getStats();\r\n const period = successRates.length;\r\n const movingAverage = calculateMovingAverage();\r\n\r\n log(4, '[health.js] GET /health [200] - returning server health.');\r\n\r\n res.send({\r\n status: 'OK',\r\n bootTime: serverStartTime,\r\n uptime:\r\n Math.floor(\r\n (new Date().getTime() - serverStartTime.getTime()) / 1000 / 60\r\n ) + ' minutes',\r\n version: pkgFile.version,\r\n highchartsVersion: version(),\r\n averageProcessingTime: stats.spentAverage,\r\n performedExports: stats.performedExports,\r\n failedExports: stats.droppedExports,\r\n exportAttempts: stats.exportAttempts,\r\n sucessRatio: (stats.performedExports / stats.exportAttempts) * 100,\r\n // eslint-disable-next-line import/no-named-as-default-member\r\n pool: pool.getPoolInfoJSON(),\r\n\r\n // Moving average\r\n period,\r\n movingAverage,\r\n message:\r\n isNaN(movingAverage) || !successRates.length\r\n ? 'Too early to report. No exports made yet. Please check back soon.'\r\n : `Last ${period} minutes had a success rate of ${movingAverage.toFixed(2)}%.`,\r\n\r\n // SVG/JSON attempts\r\n svgExportAttempts: stats.exportFromSvgAttempts,\r\n jsonExportAttempts: stats.performedExports - stats.exportFromSvgAttempts\r\n });\r\n });\r\n}\r\n","/*******************************************************************************\r\n\r\nHighcharts Export Server\r\n\r\nCopyright (c) 2016-2024, Highsoft\r\n\r\nLicenced under the MIT licence.\r\n\r\nAdditionally a valid Highcharts license is required for use.\r\n\r\nSee LICENSE file in root for details.\r\n\r\n*******************************************************************************/\r\n\r\nimport { promises as fsPromises } from 'fs';\r\nimport { posix } from 'path';\r\n\r\nimport cors from 'cors';\r\nimport express from 'express';\r\nimport http from 'http';\r\nimport https from 'https';\r\nimport multer from 'multer';\r\n\r\nimport errorHandler from './error.js';\r\nimport rateLimit from './rate_limit.js';\r\nimport { log, logWithStack } from '../logger.js';\r\nimport { __dirname } from '../utils.js';\r\n\r\nimport vSwitchRoute from './routes/change_hc_version.js';\r\nimport exportRoutes from './routes/export.js';\r\nimport healthRoute from './routes/health.js';\r\nimport uiRoute from './routes/ui.js';\r\n\r\nimport ExportError from '../errors/ExportError.js';\r\n\r\n// Array of an active servers\r\nconst activeServers = new Map();\r\n\r\n// Create express app\r\nconst app = express();\r\n\r\n// Disable the X-Powered-By header\r\napp.disable('x-powered-by');\r\n\r\n// Enable CORS support\r\napp.use(cors());\r\n\r\n// Getting a lot of RangeNotSatisfiableError exception.\r\n// Even though this is a deprecated options, let's try to set it to false.\r\napp.use((_req, res, next) => {\r\n res.set('Accept-Ranges', 'none');\r\n next();\r\n});\r\n\r\n/**\r\n * Attach error handlers to the server.\r\n *\r\n * @param {http.Server} server - The HTTP/HTTPS server instance.\r\n */\r\nconst attachServerErrorHandlers = (server) => {\r\n server.on('clientError', (error, socket) => {\r\n logWithStack(\r\n 1,\r\n error,\r\n `[server] Client error: ${error.message}, destroying socket.`\r\n );\r\n socket.destroy();\r\n });\r\n\r\n server.on('error', (error) => {\r\n logWithStack(1, error, `[server] Server error: ${error.message}`);\r\n });\r\n\r\n server.on('connection', (socket) => {\r\n socket.on('error', (error) => {\r\n logWithStack(1, error, `[server] Socket error: ${error.message}`);\r\n });\r\n });\r\n};\r\n\r\n/**\r\n * Starts an HTTP server based on the provided configuration. The `serverConfig`\r\n * object contains all server related properties (see the `server` section\r\n * in the `lib/schemas/config.js` file for a reference).\r\n *\r\n * @param {Object} serverConfig - The server configuration object.\r\n *\r\n * @throws {ExportError} - Throws an error if the server cannot be configured\r\n * and started.\r\n */\r\nexport const startServer = async (serverConfig) => {\r\n try {\r\n // TODO: Read from config/env\r\n // NOTE:\r\n // Too big limits lead to timeouts in the export process when the\r\n // rasterization timeout is set too low.\r\n const uploadLimitMiB = serverConfig.maxUploadSize || 3;\r\n const uploadLimitBytes = uploadLimitMiB * 1024 * 1024;\r\n\r\n // Enable parsing of form data (files) with Multer package\r\n const storage = multer.memoryStorage();\r\n const upload = multer({\r\n storage,\r\n limits: {\r\n fieldSize: uploadLimitBytes\r\n }\r\n });\r\n\r\n // Enable body parser\r\n app.use(express.json({ limit: uploadLimitBytes }));\r\n app.use(express.urlencoded({ extended: true, limit: uploadLimitBytes }));\r\n\r\n // Use only non-file multipart form fields\r\n app.use(upload.none());\r\n\r\n // Stop if not enabled\r\n if (!serverConfig.enable) {\r\n return false;\r\n }\r\n\r\n // Listen HTTP server\r\n if (!serverConfig.ssl.force) {\r\n // Main server instance (HTTP)\r\n const httpServer = http.createServer(app);\r\n\r\n // Attach error handlers and listen to the server\r\n attachServerErrorHandlers(httpServer);\r\n\r\n // Listen\r\n httpServer.listen(serverConfig.port, serverConfig.host);\r\n\r\n // Save the reference to HTTP server\r\n activeServers.set(serverConfig.port, httpServer);\r\n\r\n log(\r\n 3,\r\n `[server] Started HTTP server on ${serverConfig.host}:${serverConfig.port}.`\r\n );\r\n }\r\n\r\n // Listen HTTPS server\r\n if (serverConfig.ssl.enable) {\r\n // Set up an SSL server also\r\n let key, cert;\r\n\r\n try {\r\n // Get the SSL key\r\n key = await fsPromises.readFile(\r\n posix.join(serverConfig.ssl.certPath, 'server.key'),\r\n 'utf8'\r\n );\r\n\r\n // Get the SSL certificate\r\n cert = await fsPromises.readFile(\r\n posix.join(serverConfig.ssl.certPath, 'server.crt'),\r\n 'utf8'\r\n );\r\n } catch (error) {\r\n log(\r\n 2,\r\n `[server] Unable to load key/certificate from the '${serverConfig.ssl.certPath}' path. Could not run secured layer server.`\r\n );\r\n }\r\n\r\n if (key && cert) {\r\n // Main server instance (HTTPS)\r\n const httpsServer = https.createServer({ key, cert }, app);\r\n\r\n // Attach error handlers and listen to the server\r\n attachServerErrorHandlers(httpsServer);\r\n\r\n // Listen\r\n httpsServer.listen(serverConfig.ssl.port, serverConfig.host);\r\n\r\n // Save the reference to HTTPS server\r\n activeServers.set(serverConfig.ssl.port, httpsServer);\r\n\r\n log(\r\n 3,\r\n `[server] Started HTTPS server on ${serverConfig.host}:${serverConfig.ssl.port}.`\r\n );\r\n }\r\n }\r\n\r\n // Enable the rate limiter if config says so\r\n if (\r\n serverConfig.rateLimiting &&\r\n serverConfig.rateLimiting.enable &&\r\n ![0, NaN].includes(serverConfig.rateLimiting.maxRequests)\r\n ) {\r\n rateLimit(app, serverConfig.rateLimiting);\r\n }\r\n\r\n // Set up static folder's route\r\n app.use(express.static(posix.join(__dirname, 'public')));\r\n\r\n // Set up routes\r\n healthRoute(app);\r\n exportRoutes(app);\r\n uiRoute(app);\r\n vSwitchRoute(app);\r\n\r\n // Set up centralized error handler\r\n errorHandler(app);\r\n } catch (error) {\r\n throw new ExportError(\r\n '[server] Could not configure and start the server.'\r\n ).setError(error);\r\n }\r\n};\r\n\r\n/**\r\n * Closes all servers associated with Express app instance.\r\n */\r\nexport const closeServers = () => {\r\n log(4, `[server] Closing all servers.`);\r\n for (const [port, server] of activeServers) {\r\n server.close(() => {\r\n activeServers.delete(port);\r\n log(4, `[server] Closed server on port: ${port}.`);\r\n });\r\n }\r\n};\r\n\r\n/**\r\n * Get all servers associated with Express app instance.\r\n *\r\n * @returns {Array} - Servers associated with Express app instance.\r\n */\r\nexport const getServers = () => activeServers;\r\n\r\n/**\r\n * Enable rate limiting for the server.\r\n *\r\n * @param {Object} limitConfig - Configuration object for rate limiting.\r\n */\r\nexport const enableRateLimiting = (limitConfig) => rateLimit(app, limitConfig);\r\n\r\n/**\r\n * Get the Express instance.\r\n *\r\n * @returns {Object} - The Express instance.\r\n */\r\nexport const getExpress = () => express;\r\n\r\n/**\r\n * Get the Express app instance.\r\n *\r\n * @returns {Object} - The Express app instance.\r\n */\r\nexport const getApp = () => app;\r\n\r\n/**\r\n * Apply middleware(s) to a specific path.\r\n *\r\n * @param {string} path - The path to which the middleware(s) should be applied.\r\n * @param {...Function} middlewares - The middleware functions to be applied.\r\n */\r\nexport const use = (path, ...middlewares) => {\r\n app.use(path, ...middlewares);\r\n};\r\n\r\n/**\r\n * Set up a route with GET method and apply middleware(s).\r\n *\r\n * @param {string} path - The route path.\r\n * @param {...Function} middlewares - The middleware functions to be applied.\r\n */\r\nexport const get = (path, ...middlewares) => {\r\n app.get(path, ...middlewares);\r\n};\r\n\r\n/**\r\n * Set up a route with POST method and apply middleware(s).\r\n *\r\n * @param {string} path - The route path.\r\n * @param {...Function} middlewares - The middleware functions to be applied.\r\n */\r\nexport const post = (path, ...middlewares) => {\r\n app.post(path, ...middlewares);\r\n};\r\n\r\nexport default {\r\n startServer,\r\n closeServers,\r\n getServers,\r\n enableRateLimiting,\r\n getExpress,\r\n getApp,\r\n use,\r\n get,\r\n post\r\n};\r\n","/*******************************************************************************\r\n\r\nHighcharts Export Server\r\n\r\nCopyright (c) 2016-2024, Highsoft\r\n\r\nLicenced under the MIT licence.\r\n\r\nAdditionally a valid Highcharts license is required for use.\r\n\r\nSee LICENSE file in root for details.\r\n\r\n*******************************************************************************/\r\n\r\nimport { join } from 'path';\r\n\r\nimport { __dirname } from '../../utils.js';\r\n\r\n/**\r\n * Adds the GET / route for a UI when enabled on the export server.\r\n */\r\nexport default (app) =>\r\n !app\r\n ? false\r\n : app.get('/', (_request, response) => {\r\n response.sendFile(join(__dirname, 'public', 'index.html'), {\r\n acceptRanges: false\r\n });\r\n });\r\n","/*******************************************************************************\r\n\r\nHighcharts Export Server\r\n\r\nCopyright (c) 2016-2024, Highsoft\r\n\r\nLicenced under the MIT licence.\r\n\r\nAdditionally a valid Highcharts license is required for use.\r\n\r\nSee LICENSE file in root for details.\r\n\r\n*******************************************************************************/\r\n\r\nimport { clearAllIntervals } from './intervals.js';\r\nimport { killPool } from './pool.js';\r\nimport { closeServers } from './server/server.js';\r\n\r\n/**\r\n * Clean up function to trigger before ending process for the graceful shutdown.\r\n *\r\n * @param {number} exitCode - An exit code for the process.exit() function.\r\n */\r\nexport const shutdownCleanUp = async (exitCode) => {\r\n // Await freeing all resources\r\n await Promise.allSettled([\r\n // Clear all ongoing intervals\r\n clearAllIntervals(),\r\n\r\n // Get available server instances (HTTP/HTTPS) and close them\r\n closeServers(),\r\n\r\n // Close pool along with its workers and the browser instance, if exists\r\n killPool()\r\n ]);\r\n\r\n // Exit process with a correct code\r\n process.exit(exitCode);\r\n};\r\n\r\nexport default {\r\n shutdownCleanUp\r\n};\r\n","/*******************************************************************************\r\n\r\nHighcharts Export Server\r\n\r\nCopyright (c) 2016-2024, Highsoft\r\n\r\nLicenced under the MIT licence.\r\n\r\nAdditionally a valid Highcharts license is required for use.\r\n\r\nSee LICENSE file in root for details.\r\n\r\n*******************************************************************************/\r\n\r\nimport 'colors';\r\n\r\nimport { checkAndUpdateCache } from './cache.js';\r\nimport {\r\n batchExport,\r\n setAllowCodeExecution,\r\n singleExport,\r\n startExport\r\n} from './chart.js';\r\nimport { mapToNewConfig, manualConfig, setOptions } from './config.js';\r\nimport {\r\n initLogging,\r\n log,\r\n logWithStack,\r\n setLogLevel,\r\n enableFileLogging\r\n} from './logger.js';\r\nimport { initPool, killPool } from './pool.js';\r\nimport { shutdownCleanUp } from './resource_release.js';\r\nimport server, { startServer } from './server/server.js';\r\nimport { printLogo, printUsage } from './utils.js';\r\n\r\n/**\r\n * Attaches exit listeners to the process, ensuring proper cleanup of resources\r\n * and termination on exit signals. Handles 'exit', 'SIGINT', 'SIGTERM', and\r\n * 'uncaughtException' events.\r\n */\r\nconst attachProcessExitListeners = () => {\r\n log(3, '[process] Attaching exit listeners to the process.');\r\n\r\n // Handler for the 'exit'\r\n process.on('exit', (code) => {\r\n log(4, `Process exited with code ${code}.`);\r\n });\r\n\r\n // Handler for the 'SIGINT'\r\n process.on('SIGINT', async (name, code) => {\r\n log(4, `The ${name} event with code: ${code}.`);\r\n await shutdownCleanUp(0);\r\n });\r\n\r\n // Handler for the 'SIGTERM'\r\n process.on('SIGTERM', async (name, code) => {\r\n log(4, `The ${name} event with code: ${code}.`);\r\n await shutdownCleanUp(0);\r\n });\r\n\r\n // Handler for the 'SIGHUP'\r\n process.on('SIGHUP', async (name, code) => {\r\n log(4, `The ${name} event with code: ${code}.`);\r\n await shutdownCleanUp(0);\r\n });\r\n\r\n // Handler for the 'uncaughtException'\r\n process.on('uncaughtException', async (error, name) => {\r\n logWithStack(1, error, `The ${name} error.`);\r\n await shutdownCleanUp(1);\r\n });\r\n};\r\n\r\n/**\r\n * Initializes the export process. Tasks such as configuring logging, checking\r\n * cache and sources, and initializing the pool of resources happen during\r\n * this stage. Function that is required to be called before trying to export charts or setting a server. The `options` is an object that contains all options.\r\n *\r\n * @param {Object} options - All export options.\r\n *\r\n * @returns {Promise} Promise resolving to the updated export options.\r\n */\r\nconst initExport = async (options) => {\r\n // Set the allowCodeExecution per export module scope\r\n setAllowCodeExecution(\r\n options.customLogic && options.customLogic.allowCodeExecution\r\n );\r\n\r\n // Init the logging\r\n initLogging(options.logging);\r\n\r\n // Attach process' exit listeners\r\n if (options.other.listenToProcessExits) {\r\n attachProcessExitListeners();\r\n }\r\n\r\n // Check if cache needs to be updated\r\n await checkAndUpdateCache(options);\r\n\r\n // Init the pool\r\n await initPool({\r\n pool: options.pool || {\r\n minWorkers: 1,\r\n maxWorkers: 1\r\n },\r\n puppeteerArgs: options.puppeteer.args || []\r\n });\r\n\r\n // Return updated options\r\n return options;\r\n};\r\n\r\nexport default {\r\n // Server\r\n server,\r\n startServer,\r\n\r\n // Exporting\r\n initExport,\r\n singleExport,\r\n batchExport,\r\n startExport,\r\n\r\n // Pool\r\n initPool,\r\n killPool,\r\n\r\n // Other\r\n setOptions,\r\n shutdownCleanUp,\r\n\r\n // Logs\r\n log,\r\n logWithStack,\r\n setLogLevel,\r\n enableFileLogging,\r\n\r\n // Utils\r\n mapToNewConfig,\r\n manualConfig,\r\n printLogo,\r\n printUsage\r\n};\r\n"],"names":["scriptsNames","core","modules","indicators","custom","defaultConfig","puppeteer","args","value","type","description","tempDir","envLink","highcharts","version","cdnURL","useNpm","coreScripts","moduleScripts","indicatorScripts","customScripts","forceFetch","cachePath","export","infile","instr","options","outfile","constr","defaultHeight","defaultWidth","defaultScale","height","width","scale","globalOptions","themeOptions","batch","rasterizationTimeout","customLogic","allowCodeExecution","allowFileResources","customCode","callback","resources","loadConfig","legacyName","createConfig","server","maxUploadSize","enable","cliName","host","port","benchmarking","proxy","username","password","timeout","rateLimiting","maxRequests","window","delay","trustProxy","skipKey","skipToken","ssl","force","certPath","pool","minWorkers","maxWorkers","workLimit","acquireTimeout","createTimeout","destroyTimeout","idleTimeout","createRetryInterval","reaperInterval","logging","level","file","dest","toConsole","toFile","ui","route","other","nodeEnv","listenToProcessExits","noLogo","hardResetPage","browserShellMode","debug","headless","devtools","listenToConsole","dumpio","slowMo","debuggingPort","promptsConfig","name","message","initial","join","separator","instructions","choices","hint","min","max","round","absoluteProps","nestedArgs","createNestedArgs","obj","propChain","Object","keys","forEach","k","includes","entry","substring","undefined","dotenv","config","v","filterArray","z","string","transform","split","map","trim","filter","length","enum","values","refine","test","isNaN","parseFloat","envs","object","PUPPETEER_TEMP_DIR","HIGHCHARTS_VERSION","HIGHCHARTS_CDN_URL","startsWith","HIGHCHARTS_USE_NPM","HIGHCHARTS_CORE_SCRIPTS","HIGHCHARTS_MODULE_SCRIPTS","HIGHCHARTS_INDICATOR_SCRIPTS","HIGHCHARTS_FORCE_FETCH","HIGHCHARTS_CACHE_PATH","HIGHCHARTS_ADMIN_TOKEN","EXPORT_TYPE","EXPORT_CONSTR","EXPORT_DEFAULT_HEIGHT","EXPORT_DEFAULT_WIDTH","EXPORT_DEFAULT_SCALE","EXPORT_RASTERIZATION_TIMEOUT","CUSTOM_LOGIC_ALLOW_CODE_EXECUTION","CUSTOM_LOGIC_ALLOW_FILE_RESOURCES","SERVER_ENABLE","SERVER_HOST","SERVER_PORT","SERVER_MAX_UPLOAD_SIZE","SERVER_BENCHMARKING","SERVER_PROXY_HOST","SERVER_PROXY_PORT","SERVER_PROXY_USERNAME","SERVER_PROXY_PASSWORD","SERVER_PROXY_TIMEOUT","SERVER_RATE_LIMITING_ENABLE","SERVER_RATE_LIMITING_MAX_REQUESTS","SERVER_RATE_LIMITING_WINDOW","SERVER_RATE_LIMITING_DELAY","SERVER_RATE_LIMITING_TRUST_PROXY","SERVER_RATE_LIMITING_SKIP_KEY","SERVER_RATE_LIMITING_SKIP_TOKEN","SERVER_SSL_ENABLE","SERVER_SSL_FORCE","SERVER_SSL_PORT","SERVER_SSL_CERT_PATH","POOL_MIN_WORKERS","POOL_MAX_WORKERS","POOL_WORK_LIMIT","POOL_ACQUIRE_TIMEOUT","POOL_CREATE_TIMEOUT","POOL_DESTROY_TIMEOUT","POOL_IDLE_TIMEOUT","POOL_CREATE_RETRY_INTERVAL","POOL_REAPER_INTERVAL","POOL_BENCHMARKING","LOGGING_LEVEL","LOGGING_FILE","LOGGING_DEST","LOGGING_TO_CONSOLE","LOGGING_TO_FILE","UI_ENABLE","UI_ROUTE","OTHER_NODE_ENV","OTHER_LISTEN_TO_PROCESS_EXITS","OTHER_NO_LOGO","OTHER_HARD_RESET_PAGE","OTHER_BROWSER_SHELL_MODE","OTHER_ALLOW_XLINK","DEBUG_ENABLE","DEBUG_HEADLESS","DEBUG_DEVTOOLS","DEBUG_LISTEN_TO_CONSOLE","DEBUG_DUMPIO","DEBUG_SLOW_MO","DEBUG_DEBUGGING_PORT","partial","parse","process","env","colors","pathCreated","levelsDesc","title","color","listeners","logToFile","texts","prefix","existsSync","mkdirSync","appendFile","concat","error","console","log","newLevel","Date","toString","fn","apply","logWithStack","customMessage","mainMessage","stackMessage","stack","slice","setLogLevel","enableFileLogging","logDest","logFile","endsWith","__highchartsDir","dirname","createRequire","url","resolve","__dirname","fileURLToPath","URL","fixType","formats","outType","pop","find","t","handleResources","allowedProps","handledResources","correctResources","isCorrectJSON","readFileSync","files","propName","item","data","parsedData","JSON","stringify","deepCopy","copy","Array","isArray","key","prototype","hasOwnProperty","call","optionsStringify","allowFunctions","replaceAll","printUsage","bold","yellow","cycleCategories","option","entries","descName","green","i","blue","category","toUpperCase","red","toBoolean","wrapAround","replace","measureTime","start","hrtime","bigint","Number","generalOptions","getOptions","mergeConfigOptions","newOptions","mergedOptions","updateDefaultConfig","configObj","customObj","customValue","initOptions","items","recursiveProps","objectToUpdate","nestedNames","shift","assign","async","fetch","requestOptions","Promise","reject","protocol","https","http","getProtocol","get","headers","Referer","res","on","chunk","text","ExportError","Error","constructor","super","this","setError","statusCode","cache","activeManifest","sources","hcVersion","extractVersion","indexOf","extractModuleName","scriptPath","fetchAndProcessScript","script","fetchedModules","shouldThrowError","response","resolvedScriptPath","sep","updateCache","highchartsOptions","proxyOptions","sourcePath","proxyAgent","HttpsProxyAgent","agent","all","c","m","fetchScripts","writeFileSync","checkAndUpdateCache","manifestPath","requestUpdate","manifest","moduleMap","numberOfModules","some","moduleName","newManifest","saveConfigToManifest","getCachePath","setupHighcharts","Highcharts","animObject","duration","triggerExport","chartOptions","displayErrors","_displayErrors","merge","setOptions","wrap","setOptionsObj","chart","animation","strInj","isRenderComplete","Chart","proceed","userOptions","cb","exporting","enabled","plotOptions","series","label","tooltip","onHighchartsRender","addEvent","Series","Function","finalOptions","finalCallback","defaultOptions","prop","template","browser","newPage","page","setCacheEnabled","setPageContent","isClosed","$eval","element","errorMessage","innerHTML","setPageEvents","clearPageResources","injectedResources","resource","dispose","evaluate","oldCharts","charts","oldChart","destroy","scriptsToRemove","document","getElementsByTagName","stylesToRemove","linksToRemove","remove","setContent","waitUntil","addScriptTag","path","setAsConfig","totalSize","Buffer","byteLength","toFixed","puppeteerExport","exportOptions","debugger","isSVG","svgTemplate","injectedJs","js","push","content","isLocal","jsResource","injectedCss","css","cssImports","match","cssImportPath","cssResource","addStyleTag","addPageResources","size","svgElement","querySelector","chartHeight","baseVal","chartWidth","body","style","zoom","margin","viewportHeight","Math","abs","ceil","viewportWidth","x","y","getBoundingClientRect","trunc","getClipRegion","setViewport","deviceScaleFactor","outerHTML","createSVG","encoding","clip","race","screenshot","captureBeyondViewport","fullPage","optimizeForSpeed","quality","omitBackground","_resolve","setTimeout","createImage","emulateMediaType","pdf","createPDF","stats","performedExports","exportAttempts","exportFromSvgAttempts","timeSpent","droppedExports","spentAverage","poolConfig","factory","create","id","uuid","startDate","getTime","workCount","random","validate","workerHandle","close","initPool","puppeteerArgs","puppeteerOptions","enabledDebug","debugOptions","launchOptions","userDataDir","handleSIGINT","handleSIGTERM","handleSIGHUP","waitForInitialPage","defaultViewport","maxTries","tryCount","open","launch","createBrowser","parseInt","Pool","acquireTimeoutMillis","createTimeoutMillis","destroyTimeoutMillis","idleTimeoutMillis","createRetryIntervalMillis","reapIntervalMillis","propagateCreateError","r","hardReset","goto","clearPage","eventId","initialResources","acquire","promise","release","killPool","worker","used","destroyed","connected","closeBrowser","postWork","getPoolInfo","acquireCounter","payload","requestId","workStart","exportCounter","result","exportTime","getPoolInfoJSON","numFree","numUsed","available","pending","numPendingAcquires","pool$1","startExport","settings","endCallback","svg","initExportSettings","exportAsString","input","forbidden","JSDOM","DOMPurify","sanitize","ADD_TAGS","FORBID_ATTR","doStraightInject","doExport","findChartSize","precision","multiplier","pow","roundNumber","sourceHeight","sourceWidth","param","chartJson","customLogicOptions","allowCodeExecutionScoped","optionsName","stringToExport","chartJSON","intervalIds","clearAllIntervals","clearInterval","logErrorMiddleware","req","next","returnErrorMiddleware","stCode","status","json","rateLimit","app","limitConfig","msg","rateOptions","limiter","windowMs","delayMs","handler","request","format","send","default","skip","query","access_token","use","HttpError","setStatus","vSwitchRoute","post","adminToken","token","newVersion","params","updateVersion","reversedMime","png","jpeg","gif","requestsCounter","beforeRequest","afterRequest","doCallbacks","callbacks","uniqueId","callResponse","exportHandler","stopCounter","connection","remoteAddress","b64","noDownload","connectionAborted","socket","hadErrors","toLowerCase","substr","pattern","isPrivateRangeUrlFound","info","removeAllListeners","from","header","attachment","filename","pkgFile","pather","serverStartTime","successRates","addHealthRoutes","setInterval","successRatio","_","period","movingAverage","reduce","a","b","bootTime","uptime","floor","highchartsVersion","averageProcessingTime","failedExports","sucessRatio","svgExportAttempts","jsonExportAttempts","activeServers","Map","express","disable","cors","_req","set","attachServerErrorHandlers","startServer","serverConfig","uploadLimitBytes","storage","multer","memoryStorage","upload","limits","fieldSize","limit","urlencoded","extended","none","httpServer","createServer","listen","cert","fsPromises","readFile","posix","httpsServer","NaN","static","healthRoute","exportRoutes","_request","sendFile","acceptRanges","uiRoute","errorHandler","closeServers","delete","getServers","enableRateLimiting","getExpress","getApp","middlewares","shutdownCleanUp","exitCode","allSettled","exit","index","initExport","loggingOptions","initLogging","code","singleExport","batchExport","batchFunctions","pair","configIndex","findIndex","arg","fileName","loadConfigFile","showUsage","propertiesChain","argumentType","pairArgumentValue","mapToNewConfig","oldOptions","manualConfig","configFileName","configFile","choice","prompts","onSubmit","p","categories","questionsCounter","allQuestions","section","prompt","answer","module","writeFile","printLogo","packageVersion"],"mappings":"oqBAeO,MAAMA,EAAe,CAC1BC,KAAM,CAAC,aAAc,kBAAmB,iBACxCC,QAAS,CACP,QACA,MACA,QACA,YACA,uBACA,gBAEA,eACA,QACA,OACA,aACA,mBACA,eACA,cACA,UACA,UACA,cACA,WACA,UACA,YACA,cACA,YACA,sBACA,SACA,SACA,WACA,aACA,YACA,eAEA,SACA,eACA,YACA,kBACA,SACA,cACA,mBACA,eACA,kBACA,cACA,eAEA,cACA,WACA,eACA,WACA,SACA,OACA,WACA,YACA,SACA,qBACA,aACA,WACA,WACA,WACA,WACA,eACA,UACA,kBACA,oBACA,aACA,UACA,cACA,YACA,YAEFC,WAAY,CAAC,kBACbC,OAAQ,CACN,wEACA,mGAMSC,EAAgB,CAC3BC,UAAW,CACTC,KAAM,CACJC,MAAO,CACL,mCACA,kBACA,0CACA,2BACA,kCACA,kCACA,wCACA,2CACA,qBACA,4BACA,2CACA,uDACA,6BACA,yBACA,0BACA,+BACA,uBACA,uFACA,yBACA,oCACA,oBACA,0BACA,8CACA,2BACA,0BACA,6BACA,mCACA,wCACA,mCACA,2BACA,kCACA,uBACA,iBACA,yBACA,8BACA,oBACA,2BACA,eACA,6BACA,iBACA,aACA,SAEA,sBAEA,yBACA,oBACA,uBAEFC,KAAM,WACNC,YAAa,yCAEfC,QAAS,CACPH,MAAO,SACPC,KAAM,SACNG,QAAS,qBACTF,YAAa,0DAGjBG,WAAY,CACVC,QAAS,CACPN,MAAO,SACPC,KAAM,SACNG,QAAS,qBACTF,YAAa,sCAEfK,OAAQ,CACNP,MAAO,+BACPC,KAAM,SACNG,QAAS,qBACTF,YAAa,kDAEfM,OAAQ,CACNR,OAAO,EACPC,KAAM,UACNG,QAAS,qBACTF,YAAa,mDAEfO,YAAa,CACXT,MAAOR,EAAaC,KACpBQ,KAAM,WACNG,QAAS,0BACTF,YAAa,yCAEfQ,cAAe,CACbV,MAAOR,EAAaE,QACpBO,KAAM,WACNG,QAAS,4BACTF,YAAa,uCAEfS,iBAAkB,CAChBX,MAAOR,EAAaG,WACpBM,KAAM,WACNG,QAAS,+BACTF,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"} \ No newline at end of file +{"version":3,"file":"index.esm.js","sources":["../lib/schemas/config.js","../lib/envs.js","../lib/logger.js","../lib/utils.js","../lib/config.js","../lib/fetch.js","../lib/errors/ExportError.js","../lib/cache.js","../lib/highcharts.js","../lib/browser.js","../lib/export.js","../templates/svg_export/svg_export.js","../lib/errors/codes.js","../lib/pool.js","../lib/sanitize.js","../lib/chart.js","../lib/intervals.js","../lib/server/error.js","../lib/server/rate_limit.js","../lib/errors/HttpError.js","../lib/server/routes/change_hc_version.js","../lib/server/routes/export.js","../lib/server/routes/health.js","../lib/server/server.js","../lib/server/routes/ui.js","../lib/resource_release.js","../lib/index.js"],"sourcesContent":["/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\n// Possible names for Highcharts scripts\nexport const scriptsNames = {\n core: ['highcharts', 'highcharts-more', 'highcharts-3d'],\n modules: [\n 'stock',\n 'map',\n 'gantt',\n 'exporting',\n 'parallel-coordinates',\n 'accessibility',\n // 'annotations-advanced',\n 'boost-canvas',\n 'boost',\n 'data',\n 'data-tools',\n 'draggable-points',\n 'static-scale',\n 'broken-axis',\n 'heatmap',\n 'tilemap',\n 'tiledwebmap',\n 'timeline',\n 'treemap',\n 'treegraph',\n 'item-series',\n 'drilldown',\n 'histogram-bellcurve',\n 'bullet',\n 'funnel',\n 'funnel3d',\n 'geoheatmap',\n 'pyramid3d',\n 'networkgraph',\n // 'overlapping-datalabels',\n 'pareto',\n 'pattern-fill',\n 'pictorial',\n 'price-indicator',\n 'sankey',\n 'arc-diagram',\n 'dependency-wheel',\n 'series-label',\n 'series-on-point',\n 'solid-gauge',\n 'sonification',\n // 'stock-tools',\n 'streamgraph',\n 'sunburst',\n 'variable-pie',\n 'variwide',\n 'vector',\n 'venn',\n 'windbarb',\n 'wordcloud',\n 'xrange',\n 'no-data-to-display',\n 'drag-panes',\n 'debugger',\n 'dumbbell',\n 'lollipop',\n 'cylinder',\n 'organization',\n 'dotplot',\n 'marker-clusters',\n 'hollowcandlestick',\n 'heikinashi',\n 'flowmap',\n 'export-data',\n 'navigator',\n 'textpath'\n ],\n indicators: ['indicators-all'],\n custom: [\n 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js',\n 'https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js'\n ]\n};\n\n// This is the configuration object with all options and their default values,\n// also from the .env file if one exists\nexport const defaultConfig = {\n puppeteer: {\n args: {\n value: [\n '--allow-running-insecure-content',\n '--ash-no-nudges',\n '--autoplay-policy=user-gesture-required',\n '--block-new-web-contents',\n '--disable-accelerated-2d-canvas',\n '--disable-background-networking',\n '--disable-background-timer-throttling',\n '--disable-backgrounding-occluded-windows',\n '--disable-breakpad',\n '--disable-checker-imaging',\n '--disable-client-side-phishing-detection',\n '--disable-component-extensions-with-background-pages',\n '--disable-component-update',\n '--disable-default-apps',\n '--disable-dev-shm-usage',\n '--disable-domain-reliability',\n '--disable-extensions',\n '--disable-features=CalculateNativeWinOcclusion,InterestFeedContentSuggestions,WebOTP',\n '--disable-hang-monitor',\n '--disable-ipc-flooding-protection',\n '--disable-logging',\n '--disable-notifications',\n '--disable-offer-store-unmasked-wallet-cards',\n '--disable-popup-blocking',\n '--disable-print-preview',\n '--disable-prompt-on-repost',\n '--disable-renderer-backgrounding',\n '--disable-search-engine-choice-screen',\n '--disable-session-crashed-bubble',\n '--disable-setuid-sandbox',\n '--disable-site-isolation-trials',\n '--disable-speech-api',\n '--disable-sync',\n '--enable-unsafe-webgpu',\n '--hide-crash-restore-bubble',\n '--hide-scrollbars',\n '--metrics-recording-only',\n '--mute-audio',\n '--no-default-browser-check',\n '--no-first-run',\n '--no-pings',\n '--pipe',\n // '--no-sandbox',\n '--no-startup-window',\n // '--no-zygote',\n '--password-store=basic',\n '--process-per-tab',\n '--use-mock-keychain'\n ],\n type: 'string[]',\n description: 'Arguments array to send to Puppeteer.'\n },\n launchRetryWindow: {\n value: 30000,\n type: 'number',\n envLink: 'PUPPETEER_LAUNCH_RETRY_WINDOW',\n description:\n 'How long, in milliseconds, to keep retrying a browser launch before giving up. Delays between attempts grow and carry jitter. Keep this below the time an orchestrator waits before replacing an instance that has not become healthy, so that a browser which cannot start is reported rather than silently retried.'\n },\n tempDir: {\n value: './tmp/',\n type: 'string',\n envLink: 'PUPPETEER_TEMP_DIR',\n description: 'The directory for Puppeteer to store temporary files.'\n }\n },\n highcharts: {\n version: {\n value: 'latest',\n type: 'string',\n envLink: 'HIGHCHARTS_VERSION',\n description: 'The Highcharts version to be used.'\n },\n cdnURL: {\n value: 'https://code.highcharts.com/',\n type: 'string',\n envLink: 'HIGHCHARTS_CDN_URL',\n description: 'The CDN URL for Highcharts scripts to be used.'\n },\n useNpm: {\n value: false,\n type: 'boolean',\n envLink: 'HIGHCHARTS_USE_NPM',\n description: 'Flag to use Highcharts scripts from NPM package'\n },\n coreScripts: {\n value: scriptsNames.core,\n type: 'string[]',\n envLink: 'HIGHCHARTS_CORE_SCRIPTS',\n description: 'The core Highcharts scripts to fetch.'\n },\n moduleScripts: {\n value: scriptsNames.modules,\n type: 'string[]',\n envLink: 'HIGHCHARTS_MODULE_SCRIPTS',\n description: 'The modules of Highcharts to fetch.'\n },\n indicatorScripts: {\n value: scriptsNames.indicators,\n type: 'string[]',\n envLink: 'HIGHCHARTS_INDICATOR_SCRIPTS',\n description: 'The indicators of Highcharts to fetch.'\n },\n customScripts: {\n value: scriptsNames.custom,\n type: 'string[]',\n description: 'Additional custom scripts or dependencies to fetch.'\n },\n forceFetch: {\n value: false,\n type: 'boolean',\n envLink: 'HIGHCHARTS_FORCE_FETCH',\n description:\n 'The flag to determine whether to refetch all scripts after each server rerun.'\n },\n cachePath: {\n value: '.cache',\n type: 'string',\n envLink: 'HIGHCHARTS_CACHE_PATH',\n description:\n 'The path to the cache directory. It is used to store the Highcharts scripts and custom scripts.'\n }\n },\n export: {\n infile: {\n value: false,\n type: 'string',\n description:\n 'The input file should include a name and a type (json or svg). It must be correctly formatted as a JSON or SVG file.'\n },\n instr: {\n value: false,\n type: 'string',\n description:\n 'Input, provided in the form of a stringified JSON or SVG file, will override the --infile option.'\n },\n options: {\n value: false,\n type: 'string',\n description: 'An alias for the --instr option.'\n },\n outfile: {\n value: false,\n type: 'string',\n description:\n 'The output filename along with a type (jpeg, png, pdf, or svg). This will ignore the --type flag.'\n },\n type: {\n value: 'png',\n type: 'string',\n envLink: 'EXPORT_TYPE',\n description: 'The file export format. It can be jpeg, png, pdf, or svg.'\n },\n constr: {\n value: 'chart',\n type: 'string',\n envLink: 'EXPORT_CONSTR',\n description:\n 'The constructor to use. Can be chart, stockChart, mapChart, or ganttChart.'\n },\n defaultHeight: {\n value: 400,\n type: 'number',\n envLink: 'EXPORT_DEFAULT_HEIGHT',\n description:\n 'the default height of the exported chart. Used when no value is set.'\n },\n defaultWidth: {\n value: 600,\n type: 'number',\n envLink: 'EXPORT_DEFAULT_WIDTH',\n description:\n 'The default width of the exported chart. Used when no value is set.'\n },\n defaultScale: {\n value: 1,\n type: 'number',\n envLink: 'EXPORT_DEFAULT_SCALE',\n description:\n 'The default scale of the exported chart. Used when no value is set.'\n },\n height: {\n value: false,\n type: 'number',\n description:\n 'The height of the exported chart, overriding the option in the chart settings.'\n },\n width: {\n value: false,\n type: 'number',\n description:\n 'The width of the exported chart, overriding the option in the chart settings.'\n },\n scale: {\n value: false,\n type: 'number',\n description:\n 'The scale of the exported chart, overriding the option in the chart settings. Ranges between 0.1 and 5.0.'\n },\n globalOptions: {\n value: false,\n type: 'string',\n description:\n 'Either a stringified JSON or a filename containing options to be passed into the Highcharts.setOptions.'\n },\n themeOptions: {\n value: false,\n type: 'string',\n description:\n 'Either a stringified JSON or a filename containing theme options to be passed into the Highcharts.setOptions.'\n },\n batch: {\n value: false,\n type: 'string',\n description:\n 'Initiates a batch job with a string containing input/output pairs: \"in=out;in=out;...\".'\n },\n rasterizationTimeout: {\n value: 1500,\n type: 'number',\n envLink: 'EXPORT_RASTERIZATION_TIMEOUT',\n description:\n 'The duration in milliseconds to wait for rendering a webpage.'\n }\n },\n customLogic: {\n allowCodeExecution: {\n value: false,\n type: 'boolean',\n envLink: 'CUSTOM_LOGIC_ALLOW_CODE_EXECUTION',\n description:\n 'Controls whether the execution of arbitrary code is allowed during the exporting process.'\n },\n allowFileResources: {\n value: false,\n type: 'boolean',\n envLink: 'CUSTOM_LOGIC_ALLOW_FILE_RESOURCES',\n description:\n 'Controls the ability to inject resources from the filesystem. This setting has no effect when running as a server.'\n },\n customCode: {\n value: false,\n type: 'string',\n description:\n 'Custom code to execute before chart initialization. It can be a function, code wrapped within a function, or a filename with the .js extension.'\n },\n callback: {\n value: false,\n type: 'string',\n description:\n 'JavaScript code to run during construction. It can be a function or a filename with the .js extension.'\n },\n resources: {\n value: false,\n type: 'string',\n description:\n 'Additional resource in the form of a stringified JSON, which may contain files, js, and css sections.'\n },\n loadConfig: {\n value: false,\n type: 'string',\n legacyName: 'fromFile',\n description: 'A file containing a pre-defined configuration to use.'\n },\n createConfig: {\n value: false,\n type: 'string',\n description:\n 'Enables setting options through a prompt and saving them in a provided config file.'\n }\n },\n server: {\n maxUploadSize: {\n value: 3,\n type: 'number',\n envLink: 'SERVER_MAX_UPLOAD_SIZE',\n description: 'The maximum upload size, in MB, for the server.'\n },\n enable: {\n value: false,\n type: 'boolean',\n envLink: 'SERVER_ENABLE',\n cliName: 'enableServer',\n description:\n 'When set to true, the server starts on the local IP address 0.0.0.0.'\n },\n host: {\n value: '0.0.0.0',\n type: 'string',\n envLink: 'SERVER_HOST',\n description:\n 'The hostname of the server. Additionally, it starts a server on the provided hostname.'\n },\n port: {\n value: 7801,\n type: 'number',\n envLink: 'SERVER_PORT',\n description: 'The server port when enabled.'\n },\n keepAliveTimeout: {\n value: 65000,\n type: 'number',\n envLink: 'SERVER_KEEP_ALIVE_TIMEOUT',\n description:\n 'How long, in milliseconds, an idle keep-alive connection is held open. Node defaults to 5 seconds, which is shorter than the idle timeout of a typical proxy or load balancer, and the mismatch causes sporadic gateway errors when the proxy sends a request into a connection this server has just closed. Keep this above the idle timeout of anything in front of the server. headersTimeout is kept 5 seconds above it automatically.'\n },\n benchmarking: {\n value: false,\n type: 'boolean',\n envLink: 'SERVER_BENCHMARKING',\n cliName: 'serverBenchmarking',\n description:\n 'Indicates whether to display the duration, in milliseconds, of specific actions that occur on the server while serving a request.'\n },\n proxy: {\n host: {\n value: false,\n type: 'string',\n envLink: 'SERVER_PROXY_HOST',\n cliName: 'proxyHost',\n description: 'The host of the proxy server to use, if it exists.'\n },\n port: {\n value: 8080,\n type: 'number',\n envLink: 'SERVER_PROXY_PORT',\n cliName: 'proxyPort',\n description: 'The port of the proxy server to use, if it exists.'\n },\n username: {\n value: false,\n type: 'string',\n envLink: 'SERVER_PROXY_USERNAME',\n cliName: 'proxyUsername',\n description: 'The username for the proxy server, if it exists.'\n },\n password: {\n value: false,\n type: 'string',\n envLink: 'SERVER_PROXY_PASSWORD',\n cliName: 'proxyPassword',\n description: 'The password for the proxy server, if it exists.'\n },\n timeout: {\n value: 5000,\n type: 'number',\n envLink: 'SERVER_PROXY_TIMEOUT',\n cliName: 'proxyTimeout',\n description: 'The timeout for the proxy server to use, if it exists.'\n }\n },\n rateLimiting: {\n enable: {\n value: false,\n type: 'boolean',\n envLink: 'SERVER_RATE_LIMITING_ENABLE',\n cliName: 'enableRateLimiting',\n description: 'Enables rate limiting for the server.'\n },\n maxRequests: {\n value: 10,\n type: 'number',\n envLink: 'SERVER_RATE_LIMITING_MAX_REQUESTS',\n legacyName: 'rateLimit',\n description: 'The maximum number of requests allowed in one minute.'\n },\n window: {\n value: 1,\n type: 'number',\n envLink: 'SERVER_RATE_LIMITING_WINDOW',\n description: 'The time window, in minutes, for the rate limiting.'\n },\n trustProxy: {\n value: false,\n type: 'boolean',\n envLink: 'SERVER_RATE_LIMITING_TRUST_PROXY',\n description: 'Set this to true if the server is behind a load balancer.'\n },\n skipKey: {\n value: false,\n type: 'string',\n envLink: 'SERVER_RATE_LIMITING_SKIP_KEY',\n description:\n 'Allows bypassing the rate limiter and should be provided with the skipToken argument.'\n },\n skipToken: {\n value: false,\n type: 'string',\n envLink: 'SERVER_RATE_LIMITING_SKIP_TOKEN',\n description:\n 'Allows bypassing the rate limiter and should be provided with the skipKey argument.'\n }\n },\n ssl: {\n enable: {\n value: false,\n type: 'boolean',\n envLink: 'SERVER_SSL_ENABLE',\n cliName: 'enableSsl',\n description: 'Enables or disables the SSL protocol.'\n },\n force: {\n value: false,\n type: 'boolean',\n envLink: 'SERVER_SSL_FORCE',\n cliName: 'sslForce',\n legacyName: 'sslOnly',\n description:\n 'When set to true, the server is forced to serve only over HTTPS.'\n },\n port: {\n value: 443,\n type: 'number',\n envLink: 'SERVER_SSL_PORT',\n cliName: 'sslPort',\n description: 'The port on which to run the SSL server.'\n },\n certPath: {\n value: false,\n type: 'string',\n envLink: 'SERVER_SSL_CERT_PATH',\n legacyName: 'sslPath',\n description: 'The path to the SSL certificate/key file.'\n }\n }\n },\n pool: {\n minWorkers: {\n value: 4,\n type: 'number',\n envLink: 'POOL_MIN_WORKERS',\n description: 'The number of minimum and initial pool workers to spawn.'\n },\n maxWorkers: {\n value: 8,\n type: 'number',\n envLink: 'POOL_MAX_WORKERS',\n legacyName: 'workers',\n description: 'The number of maximum pool workers to spawn.'\n },\n workLimit: {\n value: 40,\n type: 'number',\n envLink: 'POOL_WORK_LIMIT',\n description:\n 'The number of work pieces that can be performed before restarting the worker process.'\n },\n queueLimit: {\n value: 0,\n type: 'number',\n envLink: 'POOL_QUEUE_LIMIT',\n description:\n 'The maximum number of exports allowed to wait for a worker. Requests arriving beyond this are refused instead of being queued. Set to 0 to derive it as four times maxWorkers. Throughput does not improve past the pool size, so a deeper queue only adds latency and memory use.'\n },\n queueRejectDelay: {\n value: 500,\n type: 'number',\n envLink: 'POOL_QUEUE_REJECT_DELAY',\n description:\n 'The duration, in milliseconds, to wait before answering a request refused because the queue is full. This is deliberate backpressure: answering instantly lets clients that retry immediately consume the whole event loop refusing them, starving the exports already in progress. Set to 0 to answer immediately, only advisable when something upstream is limiting the request rate.'\n },\n acquireTimeout: {\n value: 5000,\n type: 'number',\n envLink: 'POOL_ACQUIRE_TIMEOUT',\n description:\n 'The duration, in milliseconds, to wait for acquiring a resource.'\n },\n createTimeout: {\n value: 5000,\n type: 'number',\n envLink: 'POOL_CREATE_TIMEOUT',\n description:\n 'The duration, in milliseconds, to wait for creating a resource.'\n },\n destroyTimeout: {\n value: 5000,\n type: 'number',\n envLink: 'POOL_DESTROY_TIMEOUT',\n description:\n 'The duration, in milliseconds, to wait for destroying a resource.'\n },\n idleTimeout: {\n value: 30000,\n type: 'number',\n envLink: 'POOL_IDLE_TIMEOUT',\n description:\n 'The duration, in milliseconds, after which an idle resource is destroyed.'\n },\n createRetryInterval: {\n value: 200,\n type: 'number',\n envLink: 'POOL_CREATE_RETRY_INTERVAL',\n description:\n 'The duration, in milliseconds, to wait before retrying the create process in case of a failure.'\n },\n reaperInterval: {\n value: 1000,\n type: 'number',\n envLink: 'POOL_REAPER_INTERVAL',\n description:\n 'The duration, in milliseconds, after which the check for idle resources to destroy is triggered.'\n },\n benchmarking: {\n value: false,\n type: 'boolean',\n envLink: 'POOL_BENCHMARKING',\n cliName: 'poolBenchmarking',\n description:\n 'Indicate whether to show statistics for the pool of resources or not.'\n }\n },\n logging: {\n level: {\n value: 4,\n type: 'number',\n envLink: 'LOGGING_LEVEL',\n cliName: 'logLevel',\n description: 'The logging level to be used.'\n },\n file: {\n value: 'highcharts-export-server.log',\n type: 'string',\n envLink: 'LOGGING_FILE',\n cliName: 'logFile',\n description:\n 'The name of a log file. The `logToFile` and `logDest` options also need to be set to enable file logging.'\n },\n dest: {\n value: 'log/',\n type: 'string',\n envLink: 'LOGGING_DEST',\n cliName: 'logDest',\n description:\n 'The path to store log files. The `logToFile` option also needs to be set to enable file logging.'\n },\n toConsole: {\n value: true,\n type: 'boolean',\n envLink: 'LOGGING_TO_CONSOLE',\n cliName: 'logToConsole',\n description: 'Enables or disables showing logs in the console.'\n },\n toFile: {\n value: true,\n type: 'boolean',\n envLink: 'LOGGING_TO_FILE',\n cliName: 'logToFile',\n description:\n 'Enables or disables creation of the log directory and saving the log into a .log file.'\n }\n },\n ui: {\n enable: {\n value: false,\n type: 'boolean',\n envLink: 'UI_ENABLE',\n cliName: 'enableUi',\n description:\n 'Enables or disables the user interface (UI) for the export server.'\n },\n route: {\n value: '/',\n type: 'string',\n envLink: 'UI_ROUTE',\n cliName: 'uiRoute',\n description:\n 'The endpoint route to which the user interface (UI) should be attached.'\n }\n },\n other: {\n nodeEnv: {\n value: 'production',\n type: 'string',\n envLink: 'OTHER_NODE_ENV',\n description: 'The type of Node.js environment.'\n },\n listenToProcessExits: {\n value: true,\n type: 'boolean',\n envLink: 'OTHER_LISTEN_TO_PROCESS_EXITS',\n description: 'Decides whether or not to attach process.exit handlers.'\n },\n noLogo: {\n value: false,\n type: 'boolean',\n envLink: 'OTHER_NO_LOGO',\n description:\n 'Skip printing the logo on a startup. Will be replaced by a simple text.'\n },\n hardResetPage: {\n value: false,\n type: 'boolean',\n envLink: 'OTHER_HARD_RESET_PAGE',\n description: 'Decides if the page content should be reset entirely.'\n },\n shutdownDrainTimeout: {\n value: 30000,\n type: 'number',\n envLink: 'OTHER_SHUTDOWN_DRAIN_TIMEOUT',\n description:\n 'How long, in milliseconds, to let requests already being served finish during a shutdown, before the worker pool is taken away and the process exits. Set it above the deregistration delay of anything routing traffic to this server, so that it has stopped sending before the server stops answering.'\n },\n browserShellMode: {\n value: true,\n type: 'boolean',\n envLink: 'OTHER_BROWSER_SHELL_MODE',\n description: 'Decides if the browser runs in the shell mode.'\n }\n },\n debug: {\n enable: {\n value: false,\n type: 'boolean',\n envLink: 'DEBUG_ENABLE',\n cliName: 'enableDebug',\n description: 'Enables or disables debug mode for the underlying browser.'\n },\n headless: {\n value: true,\n type: 'boolean',\n envLink: 'DEBUG_HEADLESS',\n description:\n 'Controls the mode in which the browser is launched when in the debug mode.'\n },\n devtools: {\n value: false,\n type: 'boolean',\n envLink: 'DEBUG_DEVTOOLS',\n description:\n 'Decides whether to enable DevTools when the browser is in a headful state.'\n },\n listenToConsole: {\n value: false,\n type: 'boolean',\n envLink: 'DEBUG_LISTEN_TO_CONSOLE',\n description:\n 'Decides whether to enable a listener for console messages sent from the browser.'\n },\n dumpio: {\n value: false,\n type: 'boolean',\n envLink: 'DEBUG_DUMPIO',\n description:\n 'Redirects browser process stdout and stderr to process.stdout and process.stderr.'\n },\n slowMo: {\n value: 0,\n type: 'number',\n envLink: 'DEBUG_SLOW_MO',\n description:\n 'Slows down Puppeteer operations by the specified number of milliseconds.'\n },\n debuggingPort: {\n value: 9222,\n type: 'number',\n envLink: 'DEBUG_DEBUGGING_PORT',\n description: 'Specifies the debugging port.'\n }\n }\n};\n\n// The config descriptions object for the prompts functionality. It contains\n// information like:\n// * Type of a prompt\n// * Name of an option\n// * Short description of a chosen option\n// * Initial value\nexport const promptsConfig = {\n puppeteer: [\n {\n type: 'list',\n name: 'args',\n message: 'Puppeteer arguments',\n initial: defaultConfig.puppeteer.args.value.join(','),\n separator: ','\n }\n ],\n highcharts: [\n {\n type: 'text',\n name: 'version',\n message: 'Highcharts version',\n initial: defaultConfig.highcharts.version.value\n },\n {\n type: 'text',\n name: 'cdnURL',\n message: 'The URL of CDN',\n initial: defaultConfig.highcharts.cdnURL.value\n },\n {\n type: 'toggle',\n name: 'useNpm',\n message: 'Flag to use Highcharts scripts from NPM package',\n initial: defaultConfig.highcharts.useNpm.value\n },\n {\n type: 'multiselect',\n name: 'coreScripts',\n message: 'Available core scripts',\n instructions: 'Space: Select specific, A: Select all, Enter: Confirm.',\n choices: defaultConfig.highcharts.coreScripts.value\n },\n {\n type: 'multiselect',\n name: 'moduleScripts',\n message: 'Available module scripts',\n instructions: 'Space: Select specific, A: Select all, Enter: Confirm.',\n choices: defaultConfig.highcharts.moduleScripts.value\n },\n {\n type: 'multiselect',\n name: 'indicatorScripts',\n message: 'Available indicator scripts',\n instructions: 'Space: Select specific, A: Select all, Enter: Confirm.',\n choices: defaultConfig.highcharts.indicatorScripts.value\n },\n {\n type: 'list',\n name: 'customScripts',\n message: 'Custom scripts',\n initial: defaultConfig.highcharts.customScripts.value.join(','),\n separator: ','\n },\n {\n type: 'toggle',\n name: 'forceFetch',\n message: 'Force re-fetch the scripts',\n initial: defaultConfig.highcharts.forceFetch.value\n },\n {\n type: 'text',\n name: 'cachePath',\n message: 'The path to the cache directory',\n initial: defaultConfig.highcharts.cachePath.value\n }\n ],\n export: [\n {\n type: 'select',\n name: 'type',\n message: 'The default export file type',\n hint: `Default: ${defaultConfig.export.type.value}`,\n initial: 0,\n choices: ['png', 'jpeg', 'pdf', 'svg']\n },\n {\n type: 'select',\n name: 'constr',\n message: 'The default constructor for Highcharts',\n hint: `Default: ${defaultConfig.export.constr.value}`,\n initial: 0,\n choices: ['chart', 'stockChart', 'mapChart', 'ganttChart']\n },\n {\n type: 'number',\n name: 'defaultHeight',\n message: 'The default fallback height of the exported chart',\n initial: defaultConfig.export.defaultHeight.value\n },\n {\n type: 'number',\n name: 'defaultWidth',\n message: 'The default fallback width of the exported chart',\n initial: defaultConfig.export.defaultWidth.value\n },\n {\n type: 'number',\n name: 'defaultScale',\n message: 'The default fallback scale of the exported chart',\n initial: defaultConfig.export.defaultScale.value,\n min: 0.1,\n max: 5\n },\n {\n type: 'number',\n name: 'rasterizationTimeout',\n message: 'The rendering webpage timeout in milliseconds',\n initial: defaultConfig.export.rasterizationTimeout.value\n }\n ],\n customLogic: [\n {\n type: 'toggle',\n name: 'allowCodeExecution',\n message: 'Enable execution of custom code',\n initial: defaultConfig.customLogic.allowCodeExecution.value\n },\n {\n type: 'toggle',\n name: 'allowFileResources',\n message: 'Enable file resources',\n initial: defaultConfig.customLogic.allowFileResources.value\n }\n ],\n server: [\n {\n type: 'toggle',\n name: 'enable',\n message: 'Starts the server on 0.0.0.0',\n initial: defaultConfig.server.enable.value\n },\n {\n type: 'text',\n name: 'host',\n message: 'Server hostname',\n initial: defaultConfig.server.host.value\n },\n {\n type: 'number',\n name: 'port',\n message: 'Server port',\n initial: defaultConfig.server.port.value\n },\n {\n type: 'toggle',\n name: 'benchmarking',\n message: 'Enable server benchmarking',\n initial: defaultConfig.server.benchmarking.value\n },\n {\n type: 'text',\n name: 'proxy.host',\n message: 'The host of the proxy server to use',\n initial: defaultConfig.server.proxy.host.value\n },\n {\n type: 'number',\n name: 'proxy.port',\n message: 'The port of the proxy server to use',\n initial: defaultConfig.server.proxy.port.value\n },\n {\n type: 'number',\n name: 'proxy.timeout',\n message: 'The timeout for the proxy server to use',\n initial: defaultConfig.server.proxy.timeout.value\n },\n {\n type: 'toggle',\n name: 'rateLimiting.enable',\n message: 'Enable rate limiting',\n initial: defaultConfig.server.rateLimiting.enable.value\n },\n {\n type: 'number',\n name: 'rateLimiting.maxRequests',\n message: 'The maximum requests allowed per minute',\n initial: defaultConfig.server.rateLimiting.maxRequests.value\n },\n {\n type: 'number',\n name: 'rateLimiting.window',\n message: 'The rate-limiting time window in minutes',\n initial: defaultConfig.server.rateLimiting.window.value\n },\n {\n type: 'toggle',\n name: 'rateLimiting.trustProxy',\n message: 'Set to true if behind a load balancer',\n initial: defaultConfig.server.rateLimiting.trustProxy.value\n },\n {\n type: 'text',\n name: 'rateLimiting.skipKey',\n message:\n 'Allows bypassing the rate limiter when provided with the skipToken argument',\n initial: defaultConfig.server.rateLimiting.skipKey.value\n },\n {\n type: 'text',\n name: 'rateLimiting.skipToken',\n message:\n 'Allows bypassing the rate limiter when provided with the skipKey argument',\n initial: defaultConfig.server.rateLimiting.skipToken.value\n },\n {\n type: 'toggle',\n name: 'ssl.enable',\n message: 'Enable SSL protocol',\n initial: defaultConfig.server.ssl.enable.value\n },\n {\n type: 'toggle',\n name: 'ssl.force',\n message: 'Force serving only over HTTPS',\n initial: defaultConfig.server.ssl.force.value\n },\n {\n type: 'number',\n name: 'ssl.port',\n message: 'SSL server port',\n initial: defaultConfig.server.ssl.port.value\n },\n {\n type: 'text',\n name: 'ssl.certPath',\n message: 'The path to find the SSL certificate/key',\n initial: defaultConfig.server.ssl.certPath.value\n }\n ],\n pool: [\n {\n type: 'number',\n name: 'minWorkers',\n message: 'The initial number of workers to spawn',\n initial: defaultConfig.pool.minWorkers.value\n },\n {\n type: 'number',\n name: 'maxWorkers',\n message: 'The maximum number of workers to spawn',\n initial: defaultConfig.pool.maxWorkers.value\n },\n {\n type: 'number',\n name: 'workLimit',\n message:\n 'The pieces of work that can be performed before restarting a Puppeteer process',\n initial: defaultConfig.pool.workLimit.value\n },\n {\n type: 'number',\n name: 'queueLimit',\n message:\n 'The maximum number of exports allowed to wait for a worker, or 0 to derive it from maxWorkers',\n initial: defaultConfig.pool.queueLimit.value\n },\n {\n type: 'number',\n name: 'queueRejectDelay',\n message:\n 'The number of milliseconds to wait before refusing a request because the queue is full',\n initial: defaultConfig.pool.queueRejectDelay.value\n },\n {\n type: 'number',\n name: 'acquireTimeout',\n message: 'The number of milliseconds to wait for acquiring a resource',\n initial: defaultConfig.pool.acquireTimeout.value\n },\n {\n type: 'number',\n name: 'createTimeout',\n message: 'The number of milliseconds to wait for creating a resource',\n initial: defaultConfig.pool.createTimeout.value\n },\n {\n type: 'number',\n name: 'destroyTimeout',\n message: 'The number of milliseconds to wait for destroying a resource',\n initial: defaultConfig.pool.destroyTimeout.value\n },\n {\n type: 'number',\n name: 'idleTimeout',\n message: 'The number of milliseconds after an idle resource is destroyed',\n initial: defaultConfig.pool.idleTimeout.value\n },\n {\n type: 'number',\n name: 'createRetryInterval',\n message:\n 'The retry interval in milliseconds after a create process fails',\n initial: defaultConfig.pool.createRetryInterval.value\n },\n {\n type: 'number',\n name: 'reaperInterval',\n message:\n 'The reaper interval in milliseconds after triggering the check for idle resources to destroy',\n initial: defaultConfig.pool.reaperInterval.value\n },\n {\n type: 'toggle',\n name: 'benchmarking',\n message: 'Enable benchmarking for a resource pool',\n initial: defaultConfig.pool.benchmarking.value\n }\n ],\n logging: [\n {\n type: 'number',\n name: 'level',\n message:\n 'The log level (0: silent, 1: error, 2: warning, 3: notice, 4: verbose, 5: benchmark)',\n initial: defaultConfig.logging.level.value,\n round: 0,\n min: 0,\n max: 5\n },\n {\n type: 'text',\n name: 'file',\n message:\n 'A log file name. Set with --toFile and --logDest to enable file logging',\n initial: defaultConfig.logging.file.value\n },\n {\n type: 'text',\n name: 'dest',\n message: 'The path to a log file when the file logging is enabled',\n initial: defaultConfig.logging.dest.value\n },\n {\n type: 'toggle',\n name: 'toConsole',\n message: 'Enable logging to the console',\n initial: defaultConfig.logging.toConsole.value\n },\n {\n type: 'toggle',\n name: 'toFile',\n message: 'Enables logging to a file',\n initial: defaultConfig.logging.toFile.value\n }\n ],\n ui: [\n {\n type: 'toggle',\n name: 'enable',\n message: 'Enable UI for the export server',\n initial: defaultConfig.ui.enable.value\n },\n {\n type: 'text',\n name: 'route',\n message: 'A route to attach the UI',\n initial: defaultConfig.ui.route.value\n }\n ],\n other: [\n {\n type: 'text',\n name: 'nodeEnv',\n message: 'The type of Node.js environment',\n initial: defaultConfig.other.nodeEnv.value\n },\n {\n type: 'toggle',\n name: 'listenToProcessExits',\n message: 'Set to false to skip attaching process.exit handlers',\n initial: defaultConfig.other.listenToProcessExits.value\n },\n {\n type: 'toggle',\n name: 'noLogo',\n message: 'Skip printing the logo on startup. Replaced by simple text',\n initial: defaultConfig.other.noLogo.value\n },\n {\n type: 'toggle',\n name: 'hardResetPage',\n message: 'Decides if the page content should be reset entirely',\n initial: defaultConfig.other.hardResetPage.value\n },\n {\n type: 'toggle',\n name: 'browserShellMode',\n message: 'Decides if the browser runs in the shell mode',\n initial: defaultConfig.other.browserShellMode.value\n }\n ],\n debug: [\n {\n type: 'toggle',\n name: 'enable',\n message: 'Enables debug mode for the browser instance',\n initial: defaultConfig.debug.enable.value\n },\n {\n type: 'toggle',\n name: 'headless',\n message: 'The mode setting for the browser',\n initial: defaultConfig.debug.headless.value\n },\n {\n type: 'toggle',\n name: 'devtools',\n message: 'The DevTools for the headful browser',\n initial: defaultConfig.debug.devtools.value\n },\n {\n type: 'toggle',\n name: 'listenToConsole',\n message: 'The event listener for console messages from the browser',\n initial: defaultConfig.debug.listenToConsole.value\n },\n {\n type: 'toggle',\n name: 'dumpio',\n message: 'Redirects the browser stdout and stderr to NodeJS process',\n initial: defaultConfig.debug.dumpio.value\n },\n {\n type: 'number',\n name: 'slowMo',\n message: 'Puppeteer operations slow down in milliseconds',\n initial: defaultConfig.debug.slowMo.value\n },\n {\n type: 'number',\n name: 'debuggingPort',\n message: 'The port number for debugging',\n initial: defaultConfig.debug.debuggingPort.value\n }\n ]\n};\n\n// Absolute props that, in case of merging recursively, need to be force merged\nexport const absoluteProps = [\n 'options',\n 'globalOptions',\n 'themeOptions',\n 'resources',\n 'payload'\n];\n\n// Argument nesting level of all export server options\nexport const nestedArgs = {};\n\n/**\n * Recursively creates a chain of nested arguments from an object.\n *\n * @param {Object} obj - The object containing nested arguments.\n * @param {string} propChain - The current chain of nested properties\n * (used internally during recursion).\n */\nconst createNestedArgs = (obj, propChain = '') => {\n Object.keys(obj).forEach((k) => {\n if (!['puppeteer', 'highcharts'].includes(k)) {\n const entry = obj[k];\n if (typeof entry.value === 'undefined') {\n // Go deeper in the nested arguments\n createNestedArgs(entry, `${propChain}.${k}`);\n } else {\n // Create the chain of nested arguments\n nestedArgs[entry.cliName || k] = `${propChain}.${k}`.substring(1);\n\n // Support for the legacy, PhantomJS properties names\n if (entry.legacyName !== undefined) {\n nestedArgs[entry.legacyName] = `${propChain}.${k}`.substring(1);\n }\n }\n }\n });\n};\n\ncreateNestedArgs(defaultConfig);\n","/**\n * @fileoverview\n * This file is responsible for parsing the environment variables with the 'zod'\n * library. The parsed environment variables are then exported to be used\n * in the application as \"envs\". We should not use process.env directly\n * in the application as these would not be parsed properly.\n *\n * The environment variables are parsed and validated only once when\n * the application starts. We should write a custom validator or a transformer\n * for each of the options.\n */\n\nimport dotenv from 'dotenv';\nimport { z } from 'zod';\n\nimport { scriptsNames } from './schemas/config.js';\n\n// Load .env into environment variables.\n//\n// quiet: true suppresses the summary dotenv prints from v17 onwards. This process\n// writes structured log lines and its output is read by other tooling, so an\n// unsolicited banner on stdout is noise.\ndotenv.config({ quiet: true });\n\n// Object with custom validators and transformers, to avoid repetition\n// in the Config object\nconst v = {\n // Splits string value into elements in an array, trims every element, checks\n // if an array is correct, if it is empty, and if it is, returns undefined\n array: (filterArray) =>\n z\n .string()\n .transform((value) =>\n value\n .split(',')\n .map((value) => value.trim())\n .filter((value) => filterArray.includes(value))\n )\n .transform((value) => (value.length ? value : undefined)),\n\n // Allows only true, false and correctly parse the value to boolean\n // or no value in which case the returned value will be undefined\n boolean: () =>\n z\n .enum(['true', 'false', ''])\n .transform((value) => (value !== '' ? value === 'true' : undefined)),\n\n // Allows passed values or no value in which case the returned value will\n // be undefined\n enum: (values) =>\n z\n .enum([...values, ''])\n .transform((value) => (value !== '' ? value : undefined)),\n\n // Trims the string value and checks if it is empty or contains stringified\n // values such as false, undefined, null, NaN, if it does, returns undefined\n string: () =>\n z\n .string()\n .trim()\n .refine(\n (value) =>\n !['false', 'undefined', 'null', 'NaN'].includes(value) ||\n value === '',\n {\n error: (issue) =>\n `The string contains forbidden values, received '${issue.input}'`\n }\n )\n .transform((value) => (value !== '' ? value : undefined)),\n\n // Checks if the string is a valid path directory (path format)\n path: () =>\n z\n .string()\n .trim()\n .refine(\n (value) => {\n // Simplified regex to match both absolute and relative paths\n return /^(\\.\\/|\\.\\.\\/|\\/|[a-zA-Z]:\\\\|[a-zA-Z]:\\/)?((?:[\\w-]+)[\\\\/]?)+$/.test(\n value\n );\n },\n { error: 'The string is an invalid path directory string.' }\n ),\n\n // Allows positive numbers or no value in which case the returned value will\n // be undefined\n positiveNum: () =>\n z\n .string()\n .trim()\n .refine(\n (value) =>\n value === '' || (!isNaN(parseFloat(value)) && parseFloat(value) > 0),\n {\n error: (issue) =>\n `The value must be numeric and positive, received '${issue.input}'`\n }\n )\n .transform((value) => (value !== '' ? parseFloat(value) : undefined)),\n\n // Allows non-negative numbers or no value in which case the returned value\n // will be undefined\n nonNegativeNum: () =>\n z\n .string()\n .trim()\n .refine(\n (value) =>\n value === '' || (!isNaN(parseFloat(value)) && parseFloat(value) >= 0),\n {\n error: (issue) =>\n `The value must be numeric and non-negative, received '${issue.input}'`\n }\n )\n .transform((value) => (value !== '' ? parseFloat(value) : undefined))\n};\n\nexport const Config = z.object({\n // puppeteer\n PUPPETEER_TEMP_DIR: v.path(),\n PUPPETEER_LAUNCH_RETRY_WINDOW: v.nonNegativeNum(),\n\n // highcharts\n HIGHCHARTS_VERSION: z\n .string()\n .trim()\n .refine(\n (value) => /^(latest|\\d+(\\.\\d+){0,2})$/.test(value) || value === '',\n {\n error: (issue) =>\n `HIGHCHARTS_VERSION must be 'latest', a major version, or in the form XX.YY.ZZ, received '${issue.input}'`\n }\n )\n .transform((value) => (value !== '' ? value : undefined)),\n HIGHCHARTS_CDN_URL: z\n .string()\n .trim()\n .refine(\n (value) =>\n value.startsWith('https://') ||\n value.startsWith('http://') ||\n value === '',\n {\n error: (issue) =>\n `Invalid value for HIGHCHARTS_CDN_URL. It should start with http:// or https://, received '${issue.input}'`\n }\n )\n .transform((value) => (value !== '' ? value : undefined)),\n HIGHCHARTS_USE_NPM: v.boolean(),\n HIGHCHARTS_CORE_SCRIPTS: v.array(scriptsNames.core),\n HIGHCHARTS_MODULE_SCRIPTS: v.array(scriptsNames.modules),\n HIGHCHARTS_INDICATOR_SCRIPTS: v.array(scriptsNames.indicators),\n HIGHCHARTS_FORCE_FETCH: v.boolean(),\n HIGHCHARTS_CACHE_PATH: v.string(),\n HIGHCHARTS_ADMIN_TOKEN: v.string(),\n\n // export\n EXPORT_TYPE: v.enum(['jpeg', 'png', 'pdf', 'svg']),\n EXPORT_CONSTR: v.enum(['chart', 'stockChart', 'mapChart', 'ganttChart']),\n EXPORT_DEFAULT_HEIGHT: v.positiveNum(),\n EXPORT_DEFAULT_WIDTH: v.positiveNum(),\n EXPORT_DEFAULT_SCALE: v.positiveNum(),\n EXPORT_RASTERIZATION_TIMEOUT: v.nonNegativeNum(),\n\n // custom\n CUSTOM_LOGIC_ALLOW_CODE_EXECUTION: v.boolean(),\n CUSTOM_LOGIC_ALLOW_FILE_RESOURCES: v.boolean(),\n\n // server\n SERVER_ENABLE: v.boolean(),\n SERVER_HOST: v.string(),\n SERVER_PORT: v.positiveNum(),\n SERVER_MAX_UPLOAD_SIZE: v.positiveNum(),\n SERVER_KEEP_ALIVE_TIMEOUT: v.nonNegativeNum(),\n SERVER_BENCHMARKING: v.boolean(),\n\n // server proxy\n SERVER_PROXY_HOST: v.string(),\n SERVER_PROXY_PORT: v.positiveNum(),\n SERVER_PROXY_USERNAME: v.string(),\n SERVER_PROXY_PASSWORD: v.string(),\n SERVER_PROXY_TIMEOUT: v.nonNegativeNum(),\n\n // server rate limiting\n SERVER_RATE_LIMITING_ENABLE: v.boolean(),\n SERVER_RATE_LIMITING_MAX_REQUESTS: v.nonNegativeNum(),\n SERVER_RATE_LIMITING_WINDOW: v.nonNegativeNum(),\n SERVER_RATE_LIMITING_TRUST_PROXY: v.boolean(),\n SERVER_RATE_LIMITING_SKIP_KEY: v.string(),\n SERVER_RATE_LIMITING_SKIP_TOKEN: v.string(),\n\n // server ssl\n SERVER_SSL_ENABLE: v.boolean(),\n SERVER_SSL_FORCE: v.boolean(),\n SERVER_SSL_PORT: v.positiveNum(),\n SERVER_SSL_CERT_PATH: v.string(),\n\n // pool\n POOL_MIN_WORKERS: v.nonNegativeNum(),\n POOL_MAX_WORKERS: v.nonNegativeNum(),\n POOL_WORK_LIMIT: v.positiveNum(),\n POOL_QUEUE_LIMIT: v.nonNegativeNum(),\n POOL_QUEUE_REJECT_DELAY: v.nonNegativeNum(),\n POOL_ACQUIRE_TIMEOUT: v.nonNegativeNum(),\n POOL_CREATE_TIMEOUT: v.nonNegativeNum(),\n POOL_DESTROY_TIMEOUT: v.nonNegativeNum(),\n POOL_IDLE_TIMEOUT: v.nonNegativeNum(),\n POOL_CREATE_RETRY_INTERVAL: v.nonNegativeNum(),\n POOL_REAPER_INTERVAL: v.nonNegativeNum(),\n POOL_BENCHMARKING: v.boolean(),\n\n // logger\n LOGGING_LEVEL: z\n .string()\n .trim()\n .refine(\n (value) =>\n value === '' ||\n (!isNaN(parseFloat(value)) &&\n parseFloat(value) >= 0 &&\n parseFloat(value) <= 5),\n {\n error: (issue) =>\n `Invalid value for LOGGING_LEVEL. We only accept values from 0 to 5 as logging levels, received '${issue.input}'`\n }\n )\n .transform((value) => (value !== '' ? parseFloat(value) : undefined)),\n LOGGING_FILE: v.string(),\n LOGGING_DEST: v.string(),\n LOGGING_TO_CONSOLE: v.boolean(),\n LOGGING_TO_FILE: v.boolean(),\n\n // ui\n UI_ENABLE: v.boolean(),\n UI_ROUTE: v.string(),\n\n // other\n OTHER_SHUTDOWN_DRAIN_TIMEOUT: v.nonNegativeNum(),\n OTHER_NODE_ENV: v.enum(['development', 'production', 'test']),\n OTHER_LISTEN_TO_PROCESS_EXITS: v.boolean(),\n OTHER_NO_LOGO: v.boolean(),\n OTHER_HARD_RESET_PAGE: v.boolean(),\n OTHER_BROWSER_SHELL_MODE: v.boolean(),\n OTHER_ALLOW_XLINK: v.boolean(),\n\n // debugger\n DEBUG_ENABLE: v.boolean(),\n DEBUG_HEADLESS: v.boolean(),\n DEBUG_DEVTOOLS: v.boolean(),\n DEBUG_LISTEN_TO_CONSOLE: v.boolean(),\n DEBUG_DUMPIO: v.boolean(),\n DEBUG_SLOW_MO: v.nonNegativeNum(),\n DEBUG_DEBUGGING_PORT: v.positiveNum()\n});\n\nexport const envs = Config.partial().parse(process.env);\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { appendFile, existsSync, mkdirSync } from 'fs';\n\n// The available colors\nconst colors = ['red', 'yellow', 'blue', 'gray', 'green'];\n\n// The default logging config\nlet logging = {\n // Flags for logging status\n toConsole: true,\n toFile: false,\n pathCreated: false,\n // Log levels\n levelsDesc: [\n {\n title: 'error',\n color: colors[0]\n },\n {\n title: 'warning',\n color: colors[1]\n },\n {\n title: 'notice',\n color: colors[2]\n },\n {\n title: 'verbose',\n color: colors[3]\n },\n {\n title: 'benchmark',\n color: colors[4]\n }\n ],\n // Log listeners\n listeners: []\n};\n\n/**\n * Logs the provided texts to a file, if file logging is enabled. It creates\n * the necessary directory structure if not already created and appends the\n * content, including an optional prefix, to the specified log file.\n *\n * @param {string[]} texts - An array of texts to be logged.\n * @param {string} prefix - An optional prefix to be added to each log entry.\n */\nconst logToFile = (texts, prefix) => {\n if (!logging.pathCreated) {\n // Create if does not exist\n !existsSync(logging.dest) && mkdirSync(logging.dest);\n\n // We now assume the path is available, e.g. it's the responsibility\n // of the user to create the path with the correct access rights.\n logging.pathCreated = true;\n }\n\n // Add the content to a file\n appendFile(\n `${logging.dest}${logging.file}`,\n [prefix].concat(texts).join(' ') + '\\n',\n (error) => {\n if (error) {\n console.log(`[logger] Unable to write to log file: ${error}`);\n logging.toFile = false;\n }\n }\n );\n};\n\n/**\n * Logs a message. Accepts a variable amount of arguments. Arguments after\n * `level` will be passed directly to console.log, and/or will be joined\n * and appended to the log file.\n *\n * @param {any} args - An array of arguments where the first is the log level\n * and the rest are strings to build a message with.\n */\nexport const log = (...args) => {\n const [newLevel, ...texts] = args;\n\n // Current logging options\n const { levelsDesc, level } = logging;\n\n // Check if log level is within a correct range or is a benchmark log\n if (\n newLevel !== 5 &&\n (newLevel === 0 || newLevel > level || level > levelsDesc.length)\n ) {\n return;\n }\n\n // Get rid of the GMT text information\n const newDate = new Date().toString().split('(')[0].trim();\n\n // Create a message's prefix\n const prefix = `${newDate} [${levelsDesc[newLevel - 1].title}] -`;\n\n // Call available log listeners\n logging.listeners.forEach((fn) => {\n fn(prefix, texts.join(' '));\n });\n\n // Log to console\n if (logging.toConsole) {\n console.log.apply(\n undefined,\n [prefix.toString()[logging.levelsDesc[newLevel - 1].color]].concat(texts)\n );\n }\n\n // Log to file\n if (logging.toFile) {\n logToFile(texts, prefix);\n }\n};\n\n/**\n * Logs an error message with its stack trace. Optionally, a custom message\n * can be provided.\n *\n * @param {number} level - The log level.\n * @param {Error} error - The error object.\n * @param {string} customMessage - An optional custom message to be logged along\n * with the error.\n */\nexport const logWithStack = (newLevel, error, customMessage) => {\n // Get the main message\n const mainMessage = customMessage || error.message;\n\n // Current logging options\n const { level, levelsDesc } = logging;\n\n // Check if log level is within a correct range\n if (newLevel === 0 || newLevel > level || level > levelsDesc.length) {\n return;\n }\n\n // Get rid of the GMT text information\n const newDate = new Date().toString().split('(')[0].trim();\n\n // Create a message's prefix\n const prefix = `${newDate} [${levelsDesc[newLevel - 1].title}] -`;\n\n // If the customMessage exists, we want to display the whole stack message\n const stackMessage =\n error.message !== error.stackMessage || error.stackMessage === undefined\n ? error.stack\n : error.stack.split('\\n').slice(1).join('\\n');\n\n // Combine custom message or error message with error stack message\n const texts = [mainMessage, '\\n', stackMessage];\n\n // Log to console\n if (logging.toConsole) {\n console.log.apply(\n undefined,\n [prefix.toString()[logging.levelsDesc[newLevel - 1].color]].concat([\n mainMessage[colors[newLevel - 1]],\n '\\n',\n stackMessage\n ])\n );\n }\n\n // Call available log listeners\n logging.listeners.forEach((fn) => {\n fn(prefix, texts.join(' '));\n });\n\n // Log to file\n if (logging.toFile) {\n logToFile(texts, prefix);\n }\n};\n\n/**\n * Sets the log level to the specified value. Log levels are (0 = no logging,\n * 1 = error, 2 = warning, 3 = notice, 4 = verbose or 5 = benchmark)\n *\n * @param {number} newLevel - The new log level to be set.\n */\nexport const setLogLevel = (newLevel) => {\n if (newLevel >= 0 && newLevel <= logging.levelsDesc.length) {\n logging.level = newLevel;\n }\n};\n\n/**\n * Enables file logging with the specified destination and log file.\n *\n * @param {string} logDest - The destination path for log files.\n * @param {string} logFile - The log file name.\n */\nexport const enableFileLogging = (logDest, logFile) => {\n // Update logging options\n logging = {\n ...logging,\n dest: logDest || logging.dest,\n file: logFile || logging.file,\n toFile: true\n };\n\n if (logging.dest.length === 0) {\n return log(1, '[logger] File logging initialization: no path supplied.');\n }\n\n if (!logging.dest.endsWith('/')) {\n logging.dest += '/';\n }\n};\n\n/**\n * Initializes logging with the specified logging configuration.\n *\n * @param {Object} loggingOptions - The logging configuration object.\n */\nexport const initLogging = (loggingOptions) => {\n // Set all the logging options on our logging module object\n for (const [key, value] of Object.entries(loggingOptions)) {\n logging[key] = value;\n }\n\n // Set the log level\n setLogLevel(loggingOptions && parseInt(loggingOptions.level));\n\n // Set the log file path and name\n if (loggingOptions && loggingOptions.dest && loggingOptions.toFile) {\n enableFileLogging(\n loggingOptions.dest,\n loggingOptions.file || 'highcharts-export-server.log'\n );\n }\n};\n\n/**\n * Adds a listener function to the logging system.\n *\n * @param {function} fn - The listener function to be added.\n */\nexport const listen = (fn) => {\n logging.listeners.push(fn);\n};\n\nexport default {\n log,\n logWithStack,\n setLogLevel,\n enableFileLogging,\n initLogging,\n listen\n};\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { readFileSync } from 'fs';\nimport { createRequire } from 'module';\nimport { dirname, join } from 'path';\nimport { fileURLToPath } from 'url';\n\nimport { defaultConfig } from '../lib/schemas/config.js';\nimport { log, logWithStack } from './logger.js';\n\nconst MAX_BACKOFF_ATTEMPTS = 6;\n\n// The highcharts dependency directory path\nexport const __highchartsDir = dirname(\n createRequire(import.meta.url).resolve('highcharts/package.json')\n);\n\nexport const __dirname = fileURLToPath(new URL('../.', import.meta.url));\n\n/**\n * Clears and standardizes text by replacing multiple consecutive whitespace\n * characters with a single space and trimming any leading or trailing\n * whitespace.\n *\n * @param {string} text - The input text to be cleared.\n * @param {RegExp} [rule=/\\s\\s+/g] - The regular expression rule to match\n * multiple consecutive whitespace characters.\n * @param {string} [replacer=' '] - The string used to replace multiple\n * consecutive whitespace characters.\n *\n * @returns {string} - The cleared and standardized text.\n */\nexport const clearText = (text, rule = /\\s\\s+/g, replacer = ' ') =>\n text.replaceAll(rule, replacer).trim();\n\n/**\n * Implements an exponential backoff strategy for retrying a function until\n * a certain number of attempts are reached.\n *\n * @param {Function} fn - The function to be retried.\n * @param {number} [attempt=0] - The current attempt number.\n * @param {...any} args - Arguments to be passed to the function.\n *\n * @returns {Promise} - A promise that resolves to the result of the function\n * if successful.\n *\n * @throws {Error} - Throws an error if the maximum number of attempts\n * is reached.\n */\nexport const expBackoff = async (fn, attempt = 0, ...args) => {\n try {\n // Try to call the function\n return await fn(...args);\n } catch (error) {\n // Calculate delay in ms\n const delayInMs = 2 ** attempt * 1000;\n\n // If the attempt exceeds the maximum attempts of reapeat, throw an error\n if (++attempt >= MAX_BACKOFF_ATTEMPTS) {\n throw error;\n }\n\n // Wait given amount of time\n await new Promise((response) => setTimeout(response, delayInMs));\n log(\n 3,\n `[pool] Waited ${delayInMs}ms until next call for the resource id: ${args[0]}.`\n );\n\n // Try again\n return expBackoff(fn, attempt, ...args);\n }\n};\n\n/**\n * Fixes the export type based on MIME types and file extensions.\n *\n * @param {string} type - The original export type.\n * @param {string} outfile - The file path or name.\n *\n * @returns {string} - The corrected export type.\n */\nexport const fixType = (type, outfile) => {\n // MIME types\n const mimeTypes = {\n 'image/png': 'png',\n 'image/jpeg': 'jpeg',\n 'application/pdf': 'pdf',\n 'image/svg+xml': 'svg'\n };\n\n // Formats\n const formats = ['png', 'jpeg', 'pdf', 'svg'];\n\n // Check if type and outfile's extensions are the same\n if (outfile) {\n const outType = outfile.split('.').pop();\n\n if (outType === 'jpg') {\n type = 'jpeg';\n } else if (formats.includes(outType) && type !== outType) {\n type = outType;\n }\n }\n\n // Return a correct type\n return mimeTypes[type] || formats.find((t) => t === type) || 'png';\n};\n\n/**\n * Handles and validates resources for export.\n *\n * @param {Object|string} resources - The resources to be handled. Can be either\n * a JSON object, stringified JSON or a path to a JSON file.\n * @param {boolean} allowFileResources - Whether to allow loading resources from\n * files.\n *\n * @returns {Object|undefined} - The handled resources or undefined if no valid\n * resources are found.\n */\nexport const handleResources = (resources = false, allowFileResources) => {\n const allowedProps = ['js', 'css', 'files'];\n\n let handledResources = resources;\n let correctResources = false;\n\n // Try to load resources from a file\n if (allowFileResources && resources.endsWith('.json')) {\n try {\n handledResources = isCorrectJSON(readFileSync(resources, 'utf8'));\n } catch (error) {\n return logWithStack(2, error, `[cli] No resources found.`);\n }\n } else {\n // Try to get JSON\n handledResources = isCorrectJSON(resources);\n\n // Get rid of the files section\n if (handledResources && !allowFileResources) {\n delete handledResources.files;\n }\n }\n\n // Filter from unnecessary properties\n for (const propName in handledResources) {\n if (!allowedProps.includes(propName)) {\n delete handledResources[propName];\n } else if (!correctResources) {\n correctResources = true;\n }\n }\n\n // Check if at least one of allowed properties is present\n if (!correctResources) {\n return log(3, `[cli] No resources found.`);\n }\n\n // Handle files section\n if (handledResources.files) {\n handledResources.files = handledResources.files.map((item) => item.trim());\n if (!handledResources.files || handledResources.files.length <= 0) {\n delete handledResources.files;\n }\n }\n\n // Return resources\n return handledResources;\n};\n\n/**\n * Validates and parses JSON data. Checks if provided data is or can\n * be a correct JSON. If a primitive is provided, it is stringified and returned.\n *\n * @param {Object|string} data - The JSON data to be validated and parsed.\n * @param {boolean} toString - Whether to return a stringified representation\n * of the parsed JSON.\n *\n * @returns {Object|string|boolean} - The parsed JSON object, stringified JSON,\n * or false if validation fails.\n */\nexport function isCorrectJSON(data, toString) {\n try {\n // Get the string representation if not already before parsing\n const parsedData = JSON.parse(\n typeof data !== 'string' ? JSON.stringify(data) : data\n );\n\n // Return a stringified representation of a JSON if required\n if (typeof parsedData !== 'string' && toString) {\n return JSON.stringify(parsedData);\n }\n\n // Return a JSON\n return parsedData;\n } catch {\n return false;\n }\n}\n\n/**\n * Checks if the given item is an object.\n *\n * @param {any} item - The item to be checked.\n *\n * @returns {boolean} - True if the item is an object, false otherwise.\n */\nexport const isObject = (item) =>\n typeof item === 'object' && !Array.isArray(item) && item !== null;\n\n/**\n * Checks if the given object is empty.\n *\n * @param {Object} item - The object to be checked.\n *\n * @returns {boolean} - True if the object is empty, false otherwise.\n */\nexport const isObjectEmpty = (item) =>\n typeof item === 'object' &&\n !Array.isArray(item) &&\n item !== null &&\n Object.keys(item).length === 0;\n\n/**\n * Checks if a private IP range URL is found in the given string.\n *\n * @param {string} item - The string to be checked for a private IP range URL.\n *\n * @returns {boolean} - True if a private IP range URL is found, false\n * otherwise.\n */\nexport const isPrivateRangeUrlFound = (item) => {\n const regexPatterns = [\n /xlink:href=\"(?:http:\\/\\/|https:\\/\\/)?localhost\\b/,\n /xlink:href=\"(?:http:\\/\\/|https:\\/\\/)?10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/,\n /xlink:href=\"(?:http:\\/\\/|https:\\/\\/)?127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/,\n /xlink:href=\"(?:http:\\/\\/|https:\\/\\/)?172\\.(1[6-9]|2[0-9]|3[0-1])\\.\\d{1,3}\\.\\d{1,3}\\b/,\n /xlink:href=\"(?:http:\\/\\/|https:\\/\\/)?192\\.168\\.\\d{1,3}\\.\\d{1,3}\\b/\n ];\n\n return regexPatterns.some((pattern) => pattern.test(item));\n};\n\n/**\n * Creates a deep copy of the given object or array.\n *\n * @param {Object|Array} obj - The object or array to be deeply copied.\n *\n * @returns {Object|Array} - The deep copy of the provided object or array.\n */\nexport const deepCopy = (obj) => {\n if (obj === null || typeof obj !== 'object') {\n return obj;\n }\n\n const copy = Array.isArray(obj) ? [] : {};\n\n for (const key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n copy[key] = deepCopy(obj[key]);\n }\n }\n\n return copy;\n};\n\n/**\n * Converts the provided options object to a JSON-formatted string with the\n * option to preserve functions.\n *\n * @param {Object} options - The options object to be converted to a string.\n * @param {boolean} allowFunctions - If set to true, functions are preserved\n * in the output.\n *\n * @returns {string} - The JSON-formatted string representing the options.\n */\nexport const optionsStringify = (options, allowFunctions) => {\n const replacerCallback = (name, value) => {\n if (typeof value === 'string') {\n value = value.trim();\n\n // If allowFunctions is set to true, preserve functions\n if (\n (value.startsWith('function(') || value.startsWith('function (')) &&\n value.endsWith('}')\n ) {\n value = allowFunctions\n ? `EXP_FUN${(value + '').replaceAll(/\\n|\\t|\\r/g, ' ')}EXP_FUN`\n : undefined;\n }\n }\n\n return typeof value === 'function'\n ? `EXP_FUN${(value + '').replaceAll(/\\n|\\t|\\r/g, ' ')}EXP_FUN`\n : value;\n };\n\n // Stringify options and if required, replace special functions marks\n return JSON.stringify(options, replacerCallback).replaceAll(\n /\"EXP_FUN|EXP_FUN\"/g,\n ''\n );\n};\n\n/**\n * Prints the Highcharts Export Server logo and version information.\n *\n * @param {boolean} noLogo - If true, only prints version information without\n * the logo.\n */\nexport const printLogo = (noLogo) => {\n // Get package version either from env or from package.json\n const packageVersion = JSON.parse(\n readFileSync(join(__dirname, 'package.json'))\n ).version;\n\n // Print text only\n if (noLogo) {\n console.log(`Starting Highcharts Export Server v${packageVersion}...`);\n return;\n }\n\n // Print the logo\n console.log(\n readFileSync(__dirname + '/msg/startup.msg').toString().bold.yellow,\n `v${packageVersion}\\n`.bold\n );\n};\n\n/**\n * Prints the usage information for CLI arguments. If required, it can list\n * properties recursively\n */\nexport function printUsage() {\n const pad = 48;\n const readme = 'https://github.com/highcharts/node-export-server#readme';\n\n // Display readme information\n console.log(\n '\\nUsage of CLI arguments:'.bold,\n '\\n------',\n `\\nFor more detailed information, visit the readme at: ${readme.bold.yellow}.`\n );\n\n const cycleCategories = (options) => {\n for (const [name, option] of Object.entries(options)) {\n // If category has more levels, go further\n if (!Object.prototype.hasOwnProperty.call(option, 'value')) {\n cycleCategories(option);\n } else {\n let descName = ` --${option.cliName || name} ${\n ('<' + option.type + '>').green\n } `;\n if (descName.length < pad) {\n for (let i = descName.length; i < pad; i++) {\n descName += '.';\n }\n }\n\n // Display correctly aligned messages\n console.log(\n descName,\n option.description,\n `[Default: ${option.value.toString().bold}]`.blue\n );\n }\n }\n };\n\n // Cycle through options of each categories and display the usage info\n Object.keys(defaultConfig).forEach((category) => {\n // Only puppeteer and highcharts categories cannot be configured through CLI\n if (!['puppeteer', 'highcharts'].includes(category)) {\n console.log(`\\n${category.toUpperCase()}`.red);\n cycleCategories(defaultConfig[category]);\n }\n });\n console.log('\\n');\n}\n\n/**\n * Rounds a number to the specified precision.\n *\n * @param {number} value - The number to be rounded.\n * @param {number} precision - The number of decimal places to round to.\n *\n * @returns {number} - The rounded number.\n */\nexport const roundNumber = (value, precision = 1) => {\n const multiplier = Math.pow(10, precision || 0);\n return Math.round(+value * multiplier) / multiplier;\n};\n\n/**\n * Converts a value to a boolean.\n *\n * @param {any} item - The value to be converted to a boolean.\n *\n * @returns {boolean} - The boolean representation of the input value.\n */\nexport const toBoolean = (item) =>\n ['false', 'undefined', 'null', 'NaN', '0', ''].includes(item)\n ? false\n : !!item;\n\n/**\n * Wraps custom code to execute it safely.\n *\n * @param {string} customCode - The custom code to be wrapped.\n * @param {boolean} allowFileResources - Flag to allow loading code from a file.\n *\n * @returns {string|boolean} - The wrapped custom code or false if wrapping\n * fails.\n */\nexport const wrapAround = (customCode, allowFileResources) => {\n if (customCode && typeof customCode === 'string') {\n customCode = customCode.trim();\n\n if (customCode.endsWith('.js')) {\n return allowFileResources\n ? wrapAround(readFileSync(customCode, 'utf8'))\n : false;\n } else if (\n customCode.startsWith('function()') ||\n customCode.startsWith('function ()') ||\n customCode.startsWith('()=>') ||\n customCode.startsWith('() =>')\n ) {\n return `(${customCode})()`;\n }\n return customCode.replace(/;$/, '');\n }\n};\n\n/**\n * Utility to measure elapsed time using the Node.js process.hrtime() method.\n *\n * @returns {function(): number} - A function to calculate the elapsed time\n * in milliseconds.\n */\nexport const measureTime = () => {\n const start = process.hrtime.bigint();\n return () => Number(process.hrtime.bigint() - start) / 1000000;\n};\n\nexport default {\n __highchartsDir,\n __dirname,\n clearText,\n expBackoff,\n fixType,\n handleResources,\n isCorrectJSON,\n isObject,\n isObjectEmpty,\n isPrivateRangeUrlFound,\n optionsStringify,\n printLogo,\n printUsage,\n roundNumber,\n toBoolean,\n wrapAround,\n measureTime\n};\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { existsSync, readFileSync, promises as fsPromises } from 'fs';\n\nimport prompts from 'prompts';\n\nimport {\n absoluteProps,\n defaultConfig,\n nestedArgs,\n promptsConfig\n} from './schemas/config.js';\nimport { envs } from './envs.js';\nimport { log, logWithStack } from './logger.js';\nimport { deepCopy, isObject, printUsage, toBoolean } from './utils.js';\n\nlet generalOptions = {};\n\n/**\n * Retrieves and returns the general options for the export process.\n *\n * @returns {Object} The general options object.\n */\nexport const getOptions = () => generalOptions;\n\n/**\n * Initializes and sets the general options for the server instace, keeping\n * the principle of the options load priority. It accepts optional userOptions\n * and args from the CLI.\n *\n * @param {Object} userOptions - User-provided options for customization.\n * @param {Array} args - Command-line arguments for additional configuration\n * (CLI usage).\n *\n * @returns {Object} The updated general options object.\n */\nexport const setOptions = (userOptions, args) => {\n // Only for the CLI usage\n if (args?.length) {\n // Get the additional options from the custom JSON file\n generalOptions = loadConfigFile(args);\n }\n\n // Update the default config with a correct option values\n updateDefaultConfig(defaultConfig, generalOptions);\n\n // Set values for server's options and returns them\n generalOptions = initOptions(defaultConfig);\n\n // Apply user options if there are any\n if (userOptions) {\n // Merge user options\n generalOptions = mergeConfigOptions(\n generalOptions,\n userOptions,\n absoluteProps\n );\n }\n\n // Only for the CLI usage\n if (args?.length) {\n // Pair provided arguments\n generalOptions = pairArgumentValue(generalOptions, args, defaultConfig);\n }\n\n // Return final general options\n return generalOptions;\n};\n\n/**\n * Allows manual configuration based on specified prompts and saves\n * the configuration to a file.\n *\n * @param {string} configFileName - The name of the configuration file.\n *\n * @returns {Promise} A Promise that resolves to true once the manual\n * configuration is completed and saved.\n */\nexport const manualConfig = async (configFileName) => {\n // Prepare a config object\n let configFile = {};\n\n // Check if provided config file exists\n if (existsSync(configFileName)) {\n configFile = JSON.parse(readFileSync(configFileName, 'utf8'));\n }\n\n // Question about a configuration category\n const onSubmit = async (p, categories) => {\n let questionsCounter = 0;\n let allQuestions = [];\n\n // Create a corresponding property in the manualConfig object\n for (const section of categories) {\n // Mark each option with a section\n promptsConfig[section] = promptsConfig[section].map((option) => ({\n ...option,\n section\n }));\n\n // Collect the questions\n allQuestions = [...allQuestions, ...promptsConfig[section]];\n }\n\n await prompts(allQuestions, {\n onSubmit: async (prompt, answer) => {\n // Get the default module scripts\n if (prompt.name === 'moduleScripts') {\n answer = answer.length\n ? answer.map((module) => prompt.choices[module])\n : prompt.choices;\n\n configFile[prompt.section][prompt.name] = answer;\n } else {\n configFile[prompt.section] = recursiveProps(\n Object.assign({}, configFile[prompt.section] || {}),\n prompt.name.split('.'),\n prompt.choices ? prompt.choices[answer] : answer\n );\n }\n\n if (++questionsCounter === allQuestions.length) {\n try {\n await fsPromises.writeFile(\n configFileName,\n JSON.stringify(configFile, null, 2),\n 'utf8'\n );\n } catch (error) {\n logWithStack(\n 1,\n error,\n `[config] An error occurred while creating the ${configFileName} file.`\n );\n }\n return true;\n }\n }\n });\n\n return true;\n };\n\n // Find the categories\n const choices = Object.keys(promptsConfig).map((choice) => ({\n title: `${choice} options`,\n value: choice\n }));\n\n // Category prompt\n return prompts(\n {\n type: 'multiselect',\n name: 'category',\n message: 'Which category do you want to configure?',\n hint: 'Space: Select specific, A: Select all, Enter: Confirm.',\n instructions: '',\n choices\n },\n { onSubmit }\n );\n};\n\n/**\n * Maps old-structured (PhantomJS) options to a new configuration format\n * (Puppeteer).\n *\n * @param {Object} oldOptions - Old-structured options to be mapped.\n *\n * @returns {Object} New options structured based on the defined nestedArgs\n * mapping.\n */\nexport const mapToNewConfig = (oldOptions) => {\n const newOptions = {};\n // Cycle through old-structured options\n for (const [key, value] of Object.entries(oldOptions)) {\n const propertiesChain = nestedArgs[key] ? nestedArgs[key].split('.') : [];\n\n // Populate object in correct properties levels\n propertiesChain.reduce(\n (obj, prop, index) =>\n (obj[prop] =\n propertiesChain.length - 1 === index ? value : obj[prop] || {}),\n newOptions\n );\n }\n return newOptions;\n};\n\n/**\n * Merges two sets of configuration options, considering absolute properties.\n *\n * @param {Object} options - Original configuration options.\n * @param {Object} newOptions - New configuration options to be merged.\n * @param {Array} absoluteProps - List of properties that should\n * not be recursively merged.\n *\n * @returns {Object} Merged configuration options.\n */\nexport const mergeConfigOptions = (options, newOptions, absoluteProps = []) => {\n const mergedOptions = deepCopy(options);\n\n for (const [key, value] of Object.entries(newOptions)) {\n mergedOptions[key] =\n isObject(value) &&\n !absoluteProps.includes(key) &&\n mergedOptions[key] !== undefined\n ? mergeConfigOptions(mergedOptions[key], value, absoluteProps)\n : value !== undefined\n ? value\n : mergedOptions[key];\n }\n\n return mergedOptions;\n};\n\n/**\n * Initializes export settings based on provided exportOptions\n * and generalOptions.\n *\n * @param {Object} exportOptions - Options specific to the export process.\n * @param {Object} generalOptions - General configuration options.\n *\n * @returns {Object} Initialized export settings.\n */\nexport const initExportSettings = (exportOptions, generalOptions = {}) => {\n let options = {};\n\n if (exportOptions.svg) {\n options = deepCopy(generalOptions);\n options.export.type = exportOptions.type || exportOptions.export.type;\n options.export.scale = exportOptions.scale || exportOptions.export.scale;\n options.export.outfile =\n exportOptions.outfile || exportOptions.export.outfile;\n options.payload = {\n svg: exportOptions.svg\n };\n } else {\n options = mergeConfigOptions(\n generalOptions,\n exportOptions,\n // Omit going down recursively with the belows\n absoluteProps\n );\n }\n\n options.export.outfile =\n options.export?.outfile || `chart.${options.export?.type || 'png'}`;\n return options;\n};\n\n/**\n * Loads additional configuration from a specified file using\n * the --loadConfig option.\n *\n * @param {Array} args - Command-line arguments to check for\n * the --loadConfig option.\n *\n * @returns {Object} Additional configuration loaded from the specified file,\n * or an empty object if not found or invalid.\n */\nfunction loadConfigFile(args) {\n // Check if the --loadConfig option was used\n const configIndex = args.findIndex(\n (arg) => arg.replace(/-/g, '') === 'loadConfig'\n );\n\n // Check if the --loadConfig has a value\n if (configIndex > -1 && args[configIndex + 1]) {\n const fileName = args[configIndex + 1];\n try {\n // Check if an additional config file is a correct JSON file\n if (fileName && fileName.endsWith('.json')) {\n // Load an optional custom JSON config file\n return JSON.parse(readFileSync(fileName));\n }\n } catch (error) {\n logWithStack(\n 2,\n error,\n `[config] Unable to load the configuration from the ${fileName} file.`\n );\n }\n }\n\n // No additional options to return\n return {};\n}\n\n/**\n * Updates the default configuration object with values from a custom object\n * and environment variables.\n *\n * @param {Object} configObj - The default configuration object.\n * @param {Object} customObj - Custom configuration object to override defaults.\n * @param {string} propChain - Property chain for tracking nested properties\n * during recursion.\n */\nfunction updateDefaultConfig(configObj, customObj = {}, propChain = '') {\n Object.keys(configObj).forEach((key) => {\n const entry = configObj[key];\n const customValue = customObj && customObj[key];\n\n if (typeof entry.value === 'undefined') {\n updateDefaultConfig(entry, customValue, `${propChain}.${key}`);\n } else {\n // If a value from a custom JSON exists, it take precedence\n if (customValue !== undefined) {\n entry.value = customValue;\n }\n\n // If a value from an env variable exists, it take precedence\n if (entry.envLink in envs && envs[entry.envLink] !== undefined) {\n entry.value = envs[entry.envLink];\n }\n }\n });\n}\n\n/**\n * Initializes options object based on provided items, setting values from\n * nested properties recursively.\n *\n * @param {Object} items - Configuration items to be used for initializing\n * options.\n *\n * @returns {Object} Initialized options object.\n */\nfunction initOptions(items) {\n let options = {};\n for (const [name, item] of Object.entries(items)) {\n options[name] = Object.prototype.hasOwnProperty.call(item, 'value')\n ? item.value\n : initOptions(item);\n }\n return options;\n}\n\n/**\n * Pairs argument values with corresponding options in the configuration,\n * updating the options object.\n *\n * @param {Object} options - Configuration options object to be updated.\n * @param {Array} args - Command-line arguments containing values for specific\n * options.\n * @param {Object} defaultConfig - Default configuration object for reference.\n *\n * @returns {Object} Updated options object.\n */\nfunction pairArgumentValue(options, args, defaultConfig) {\n let showUsage = false;\n for (let i = 0; i < args.length; i++) {\n const option = args[i].replace(/-/g, '');\n\n // Find the right place for property's value\n const propertiesChain = nestedArgs[option]\n ? nestedArgs[option].split('.')\n : [];\n\n // Get the correct type for CLI args which are passed as strings\n let argumentType;\n propertiesChain.reduce((obj, prop, index) => {\n if (propertiesChain.length - 1 === index) {\n argumentType = obj[prop].type;\n }\n return obj[prop];\n }, defaultConfig);\n\n propertiesChain.reduce((obj, prop, index) => {\n if (propertiesChain.length - 1 === index) {\n // Finds an option and set a corresponding value\n if (typeof obj[prop] !== 'undefined') {\n if (args[++i]) {\n if (argumentType === 'boolean') {\n obj[prop] = toBoolean(args[i]);\n } else if (argumentType === 'number') {\n obj[prop] = +args[i];\n } else if (argumentType.indexOf(']') >= 0) {\n obj[prop] = args[i].split(',');\n } else {\n obj[prop] = args[i];\n }\n } else {\n log(\n 2,\n `[config] Missing value for the '${option}' argument. Using the default value.`\n );\n showUsage = true;\n }\n }\n }\n return obj[prop];\n }, options);\n }\n\n // Display the usage for the reference if needed\n if (showUsage) {\n printUsage(defaultConfig);\n }\n\n return options;\n}\n\n/**\n * Recursively updates properties in an object based on nested names and assigns\n * the final value.\n *\n * @param {Object} objectToUpdate - The object to be updated.\n * @param {Array} nestedNames - Array of nested property names.\n * @param {any} value - The final value to be assigned.\n *\n * @returns {Object} Updated object with assigned values.\n */\nfunction recursiveProps(objectToUpdate, nestedNames, value) {\n while (nestedNames.length > 1) {\n const propName = nestedNames.shift();\n\n // Create a property in object if it doesn't exist\n if (!Object.prototype.hasOwnProperty.call(objectToUpdate, propName)) {\n objectToUpdate[propName] = {};\n }\n\n // Call function again if there still names to go\n objectToUpdate[propName] = recursiveProps(\n Object.assign({}, objectToUpdate[propName]),\n nestedNames,\n value\n );\n\n return objectToUpdate;\n }\n\n // Assign the final value\n objectToUpdate[nestedNames[0]] = value;\n return objectToUpdate;\n}\n\nexport default {\n getOptions,\n setOptions,\n manualConfig,\n mapToNewConfig,\n mergeConfigOptions,\n initExportSettings\n};\n","/**\n * This module exports two functions: fetch (for GET requests) and post (for POST requests).\n */\n\nimport http from 'http';\nimport https from 'https';\n\n/**\n * Returns the HTTP or HTTPS protocol module based on the provided URL.\n *\n * @param {string} url - The URL to determine the protocol.\n *\n * @returns {Object} The HTTP or HTTPS protocol module (http or https).\n */\nconst getProtocol = (url) => (url.startsWith('https') ? https : http);\n\n/**\n * Fetches data from the specified URL using either HTTP or HTTPS protocol.\n *\n * @param {string} url - The URL to fetch data from.\n * @param {Object} requestOptions - Options for the HTTP request (optional).\n *\n * @returns {Promise} Promise resolving to the HTTP response object\n * with added 'text' property or rejecting with an error.\n */\nasync function fetch(url, requestOptions = {}) {\n return new Promise((resolve, reject) => {\n const protocol = getProtocol(url);\n\n protocol\n .get(\n url,\n Object.assign(\n {\n headers: {\n 'User-Agent': 'highcharts/export',\n Referer: 'highcharts.export'\n }\n },\n requestOptions || {}\n ),\n (res) => {\n let data = '';\n\n // A chunk of data has been received.\n res.on('data', (chunk) => {\n data += chunk;\n });\n\n // The whole response has been received.\n res.on('end', () => {\n if (!data) {\n reject('Nothing was fetched from the URL.');\n }\n\n res.text = data;\n resolve(res);\n });\n }\n )\n .on('error', (error) => {\n reject(error);\n });\n });\n}\n\n/**\n * Sends a POST request to the specified URL with the provided JSON body using\n * either HTTP or HTTPS protocol.\n *\n * @param {string} url - The URL to send the POST request to.\n * @param {Object} body - The JSON body to include in the POST request\n * (optional, default is an empty object).\n * @param {Object} requestOptions - Options for the HTTP request (optional).\n *\n * @returns {Promise} Promise resolving to the HTTP response object with\n * added 'text' property or rejecting with an error.\n */\nasync function post(url, body = {}, requestOptions = {}) {\n return new Promise((resolve, reject) => {\n const protocol = getProtocol(url);\n const data = JSON.stringify(body);\n\n // Set default headers and merge with requestOptions\n const options = Object.assign(\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': data.length\n }\n },\n requestOptions\n );\n\n const req = protocol\n .request(url, options, (res) => {\n let responseData = '';\n\n // A chunk of data has been received.\n res.on('data', (chunk) => {\n responseData += chunk;\n });\n\n // The whole response has been received.\n res.on('end', () => {\n try {\n res.text = responseData;\n resolve(res);\n } catch (error) {\n reject(error);\n }\n });\n })\n .on('error', (error) => {\n reject(error);\n });\n\n // Write the request body and end the request.\n req.write(data);\n req.end();\n });\n}\n\nexport default fetch;\nexport { fetch, post };\n","class ExportError extends Error {\n // NOTE: Deliberately takes only a message. Two existing call sites in cache.js\n // pass a number as a second argument, intending a status code, which\n // this constructor has always ignored. Accepting a second parameter here\n // would silently give those a meaning. Use setCode() instead, which is\n // explicit.\n constructor(message) {\n super();\n this.message = message;\n this.stackMessage = message;\n }\n\n setError(error) {\n this.error = error;\n if (error.name) {\n this.name = error.name;\n }\n if (error.statusCode) {\n this.statusCode = error.statusCode;\n }\n // NOTE: Carry a machine readable code up from the wrapped error. Errors are\n // wrapped as they travel up the stack, and without this the reason a\n // request failed would be lost at the first wrap, leaving only the\n // message to tell a capacity problem from a bad request.\n if (error.errorCode && !this.errorCode) {\n this.errorCode = error.errorCode;\n }\n if (error.stack) {\n this.stackMessage = error.message;\n this.stack = error.stack;\n }\n return this;\n }\n\n setCode(errorCode) {\n this.errorCode = errorCode;\n return this;\n }\n}\n\nexport default ExportError;\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\n// The cache manager manages the Highcharts library and its dependencies.\n// The cache itself is stored in .cache, and is checked by the config system\n// before starting the service\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join, resolve, sep } from 'path';\n\nimport { HttpsProxyAgent } from 'https-proxy-agent';\n\nimport { getOptions } from './config.js';\nimport { envs } from './envs.js';\nimport { fetch } from './fetch.js';\nimport { log } from './logger.js';\nimport { __dirname, __highchartsDir } from './utils.js';\n\nimport ExportError from './errors/ExportError.js';\n\nconst cache = {\n cdnURL: 'https://code.highcharts.com/',\n activeManifest: {},\n sources: '',\n hcVersion: ''\n};\n\n/**\n * Extracts and caches the Highcharts version from the sources string.\n *\n * @returns {string} The extracted Highcharts version.\n */\nexport const extractVersion = (cache) => {\n return cache.sources\n .substring(0, cache.sources.indexOf('*/'))\n .replace('/*', '')\n .replace('*/', '')\n .replace(/\\n/g, '')\n .trim();\n};\n\n/**\n * Extracts the Highcharts module name based on the scriptPath.\n *\n * @param {string} scriptPath - The path to the module.\n *\n * @returns {string} The name of a module.\n */\nexport const extractModuleName = (scriptPath) => {\n // Normalize slashes, get part after the last '/' and remove .js extension\n return scriptPath.replace(/\\\\/g, '/').split('/').pop().replace(/\\.js$/i, '');\n};\n\n/**\n * Saves the provided configuration and fetched modules to the cache manifest\n * file.\n *\n * @param {object} config - Highcharts-related configuration object.\n * @param {object} fetchedModules - An object that contains mapped names of\n * fetched Highcharts modules to use.\n *\n * @throws {ExportError} Throws an ExportError if an error occurs while writing\n * the cache manifest.\n */\nexport const saveConfigToManifest = async (config, fetchedModules) => {\n const newManifest = {\n version: config.version,\n modules: fetchedModules || {}\n };\n\n // Update cache object with the current modules\n cache.activeManifest = newManifest;\n\n log(3, '[cache] Writing a new manifest.');\n try {\n writeFileSync(\n join(__dirname, config.cachePath, 'manifest.json'),\n JSON.stringify(newManifest),\n 'utf8'\n );\n } catch (error) {\n throw new ExportError('[cache] Error writing the cache manifest.').setError(\n error\n );\n }\n};\n\n/**\n * Fetches a single script and updates the fetchedModules accordingly.\n *\n * @param {string} script - A path to script to get.\n * @param {Object} requestOptions - Additional options for the proxy agent\n * to use for a request.\n * @param {Object} fetchedModules - An object which tracks which Highcharts\n * modules have been fetched.\n * @param {boolean} [useNpm=false] - A flag to indicate if the script should be\n * get from NPM package or fetched from CDN. The default value is `false`.\n * @param {boolean} [shouldThrowError=false] - A flag to indicate if the error\n * should be thrown. This should be used only for the core scripts.\n *\n * @returns {Promise} A Promise resolving to the text representation\n * of the fetched script.\n *\n * @throws {ExportError} Throws an ExportError if there is a problem with\n * fetching the script.\n */\nexport const fetchAndProcessScript = async (\n script,\n requestOptions,\n fetchedModules,\n useNpm = false,\n shouldThrowError = false\n) => {\n let response;\n\n // Add the missing .js to the strings\n if (!script.endsWith('.js')) {\n script = `${script}.js`;\n }\n\n // Whether to use NPM package scripts or fetch it from CDN\n if (useNpm) {\n try {\n // Log fetched script\n log(\n 4,\n `[cache] Fetching script from NPM - ${join('node_modules', 'highcharts', script)}`\n );\n\n // Sanitize and validate path\n const resolvedScriptPath = resolve(__highchartsDir, script);\n if (!resolvedScriptPath.startsWith(resolve(__highchartsDir) + sep)) {\n throw new ExportError(\n `[cache] Invalid script path detected for '${script}'. Directory traversal attempt or bad version input.`,\n 403\n );\n }\n\n // Fetch the script from NPM\n response = readFileSync(resolvedScriptPath, 'utf8');\n\n // If OK, return its text representation\n if (fetchedModules && response) {\n fetchedModules[extractModuleName(script)] = 1;\n }\n return response;\n } catch {\n // Proceed\n }\n } else {\n // Log fetched script\n log(4, `[cache] Fetching script from CDN - ${script}`);\n\n // Fetch the script from CDN\n response = await fetch(script, requestOptions);\n\n // If OK, return its text representation\n if (response.statusCode === 200 && typeof response.text == 'string') {\n if (fetchedModules) {\n fetchedModules[extractModuleName(script)] = 1;\n }\n return response.text;\n }\n }\n\n // Based on the `shouldThrowError` flag, decide how to serve error message\n if (shouldThrowError) {\n throw new ExportError(\n `[cache] Could not fetch the mandatory ${script}. The script might not exist in the requested version.`,\n 404\n );\n } else {\n log(\n 2,\n `[cache] Could not fetch the ${script}. The script might not exist in the requested version.`\n );\n }\n\n return '';\n};\n\n/**\n * Fetches Highcharts scripts and customScripts from the given CDNs.\n *\n * @param {Object} highchartsOptions - Object containing all highcharts options.\n * @param {object} proxyOptions - Options for the proxy agent to use for\n * a request.\n * @param {object} fetchedModules - An object which tracks which Highcharts\n * modules have been fetched.\n *\n * @returns {Promise} The fetched scripts content joined.\n */\nexport const fetchScripts = async (\n highchartsOptions,\n proxyOptions,\n fetchedModules\n) => {\n const version = highchartsOptions.version;\n const hcVersion = version === 'latest' || !version ? '' : `${version}/`;\n const cdnURL = highchartsOptions.cdnURL || cache.cdnURL;\n\n log(\n 3,\n `[cache] Updating cache version to Highcharts: ${hcVersion || 'latest'}.`\n );\n\n // Whether to use NPM or CDN\n const useNpm = highchartsOptions.useNpm;\n\n // Configure proxy if exists\n let proxyAgent;\n const { host, port, username, password } = proxyOptions;\n\n // Try to create a Proxy Agent\n if (host && port) {\n try {\n proxyAgent = new HttpsProxyAgent({\n host,\n port,\n ...(username && password ? { username, password } : {})\n });\n } catch (error) {\n throw new ExportError('[cache] Could not create a Proxy Agent.').setError(\n error\n );\n }\n }\n\n // If exists, add proxy agent to request options\n const requestOptions = proxyAgent\n ? {\n agent: proxyAgent,\n timeout: envs.SERVER_PROXY_TIMEOUT\n }\n : {};\n\n const fetchedScripts = await Promise.all([\n ...highchartsOptions.coreScripts.map((c) =>\n fetchAndProcessScript(\n (useNpm && c) || `${cdnURL}${hcVersion}${c}`,\n requestOptions,\n fetchedModules,\n useNpm,\n true\n )\n ),\n ...highchartsOptions.moduleScripts.map((m) =>\n fetchAndProcessScript(\n (useNpm && join('modules', m)) ||\n (m === 'map'\n ? `${cdnURL}maps/${hcVersion}modules/${m}`\n : `${cdnURL}${hcVersion}modules/${m}`),\n requestOptions,\n fetchedModules,\n useNpm\n )\n ),\n ...highchartsOptions.indicatorScripts.map((i) =>\n fetchAndProcessScript(\n (useNpm && join('indicators', i)) ||\n `${cdnURL}stock/${hcVersion}indicators/${i}`,\n requestOptions,\n fetchedModules,\n useNpm\n )\n ),\n ...highchartsOptions.customScripts.map((c) =>\n fetchAndProcessScript(`${c}`, requestOptions)\n )\n ]);\n\n return fetchedScripts.join(';\\n');\n};\n\n/**\n * Updates the local cache with Highcharts scripts and their versions.\n *\n * @param {Object} highchartsOptions - Object containing all options from\n * the highcharts section.\n * @param {string} sourcePath - The path to the source file in the cache.\n *\n * @returns {Promise} A Promise resolving to an object representing\n * the fetched modules.\n *\n * @throws {ExportError} Throws an ExportError if there is an issue updating\n * the local Highcharts cache.\n */\nexport const updateCache = async (\n highchartsOptions,\n proxyOptions,\n sourcePath\n) => {\n try {\n const fetchedModules = {};\n\n // Get sources\n cache.sources = await fetchScripts(\n highchartsOptions,\n proxyOptions,\n fetchedModules\n );\n\n // Get sources version\n cache.hcVersion = extractVersion(cache);\n\n // Save the fetched modules into caches' source JSON\n writeFileSync(sourcePath, cache.sources);\n\n return fetchedModules;\n } catch (error) {\n throw new ExportError(\n '[cache] Unable to update the local Highcharts cache.'\n ).setError(error);\n }\n};\n\n/**\n * Updates the Highcharts version in the applied configuration and checks\n * the cache for the new version.\n *\n * @param {string} newVersion - The new Highcharts version to be applied.\n *\n * @returns {Promise<(object|boolean)>} A Promise resolving to the updated\n * configuration with the new version, or false if no applied configuration\n * exists.\n */\nexport const updateVersion = async (newVersion) => {\n const options = getOptions();\n if (options?.highcharts) {\n options.highcharts.version = newVersion;\n }\n await checkAndUpdateCache(options);\n};\n\n/**\n * Checks the cache for Highcharts dependencies, updates the cache if needed,\n * and loads the sources.\n *\n * @param {Object} options - Object containing all options.\n *\n * @returns {Promise} A Promise that resolves once the cache is checked\n * and updated.\n *\n * @throws {ExportError} Throws an ExportError if there is an issue updating\n * or reading the cache.\n */\nexport const checkAndUpdateCache = async (options) => {\n const { highcharts, server } = options;\n const cachePath = join(__dirname, highcharts.cachePath);\n\n let fetchedModules;\n // Prepare paths to manifest and sources from the .cache folder\n const manifestPath = join(cachePath, 'manifest.json');\n const sourcePath = join(cachePath, 'sources.js');\n\n // Create the cache destination if it doesn't exist already\n !existsSync(cachePath) && mkdirSync(cachePath);\n\n // Fetch all the scripts either if manifest.json does not exist\n // or if the forceFetch option is enabled\n if (!existsSync(manifestPath) || highcharts.forceFetch) {\n log(3, '[cache] Fetching and caching Highcharts dependencies.');\n fetchedModules = await updateCache(highcharts, server.proxy, sourcePath);\n } else {\n let requestUpdate = false;\n\n // Read the manifest JSON\n const manifest = JSON.parse(readFileSync(manifestPath));\n\n // Check if the modules is an array, if so, we rewrite it to a map to make\n // it easier to resolve modules.\n if (manifest.modules && Array.isArray(manifest.modules)) {\n const moduleMap = {};\n manifest.modules.forEach((m) => (moduleMap[m] = 1));\n manifest.modules = moduleMap;\n }\n\n const { coreScripts, moduleScripts, indicatorScripts } = highcharts;\n const numberOfModules =\n coreScripts.length + moduleScripts.length + indicatorScripts.length;\n\n // Compare the loaded highcharts config with the contents in cache.\n // If there are changes, fetch requested modules and products,\n // and bake them into a giant blob. Save the blob.\n if (manifest.version !== highcharts.version) {\n log(\n 2,\n '[cache] A Highcharts version mismatch in the cache, need to re-fetch.'\n );\n requestUpdate = true;\n } else if (Object.keys(manifest.modules || {}).length !== numberOfModules) {\n log(\n 2,\n '[cache] The cache and the requested modules do not match, need to re-fetch.'\n );\n requestUpdate = true;\n } else {\n // Check each module, if anything is missing refetch everything\n requestUpdate = (moduleScripts || []).some((moduleName) => {\n if (!manifest.modules[moduleName]) {\n log(\n 2,\n `[cache] The ${moduleName} is missing in the cache, need to re-fetch.`\n );\n return true;\n }\n });\n }\n\n if (requestUpdate) {\n fetchedModules = await updateCache(highcharts, server.proxy, sourcePath);\n } else {\n log(3, '[cache] Dependency cache is up to date, proceeding.');\n\n // Load the sources\n cache.sources = readFileSync(sourcePath, 'utf8');\n\n // Get current modules map\n fetchedModules = manifest.modules;\n\n cache.hcVersion = extractVersion(cache);\n }\n }\n\n // Finally, save the new manifest, which is basically our current config\n // in a slightly different format\n await saveConfigToManifest(highcharts, fetchedModules);\n};\n\nexport const getCachePath = () =>\n join(__dirname, getOptions().highcharts.cachePath);\n\nexport const getCache = () => cache;\n\nexport const highcharts = () => cache.sources;\n\nexport const version = () => cache.hcVersion;\n\nexport default {\n checkAndUpdateCache,\n getCachePath,\n updateVersion,\n getCache,\n highcharts,\n version\n};\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\n/* eslint-disable no-undef */\n\n/**\n * Setting the animObject. Called when initing the page.\n */\nexport function setupHighcharts() {\n Highcharts.animObject = function () {\n return { duration: 0 };\n };\n}\n\n/**\n * Creates the actual chart.\n *\n * @param {object} chartOptions - The options for the Highcharts chart.\n * @param {object} options - The export options.\n * @param {boolean} displayErrors - A flag indicating whether to display errors.\n */\nexport async function triggerExport(chartOptions, options, displayErrors) {\n // Display errors flag taken from chart options nad debugger module\n window._displayErrors = displayErrors;\n\n // Get required functions\n const { getOptions, merge, setOptions, wrap } = Highcharts;\n\n // Create a separate object for a potential setOptions usages in order to\n // prevent from polluting other exports that can happen on the same page\n Highcharts.setOptionsObj = merge(false, {}, getOptions());\n\n // By default animation is disabled\n const chart = {\n animation: false\n };\n\n // When straight inject, the size is set through CSS only\n if (options.export.strInj) {\n chart.height = chartOptions.chart.height;\n chart.width = chartOptions.chart.width;\n }\n\n // NOTE: Is this used for anything useful?\n window.isRenderComplete = false;\n wrap(Highcharts.Chart.prototype, 'init', function (proceed, userOptions, cb) {\n // Override userOptions with image friendly options\n userOptions = merge(userOptions, {\n exporting: {\n enabled: false\n },\n plotOptions: {\n series: {\n label: {\n enabled: false\n }\n }\n },\n /* Expects tooltip in userOptions when forExport is true.\n https://github.com/highcharts/highcharts/blob/3ad430a353b8056b9e764aa4e5cd6828aa479db2/js/parts/Chart.js#L241\n */\n tooltip: {}\n });\n\n (userOptions.series || []).forEach(function (series) {\n series.animation = false;\n });\n\n // Add flag to know if chart render has been called.\n if (!window.onHighchartsRender) {\n window.onHighchartsRender = Highcharts.addEvent(this, 'render', () => {\n window.isRenderComplete = true;\n });\n }\n\n proceed.apply(this, [userOptions, cb]);\n });\n\n wrap(Highcharts.Series.prototype, 'init', function (proceed, chart, options) {\n proceed.apply(this, [chart, options]);\n });\n\n // Get the user options\n const userOptions = options.export.strInj\n ? new Function(`return ${options.export.strInj}`)()\n : chartOptions;\n\n // Trigger custom code\n if (options.customLogic.customCode) {\n new Function('options', options.customLogic.customCode)(userOptions);\n }\n\n // Merge the globalOptions, themeOptions, options from the wrapped\n // setOptions function and user options to create the final options object\n const finalOptions = merge(\n false,\n JSON.parse(options.export.themeOptions),\n userOptions,\n // Placed it here instead in the init because of the size issues\n { chart }\n );\n\n const finalCallback = options.customLogic.callback\n ? new Function(`return ${options.customLogic.callback}`)()\n : undefined;\n\n // Set the global options if exist\n const globalOptions = JSON.parse(options.export.globalOptions);\n if (globalOptions) {\n setOptions(globalOptions);\n }\n\n let constr = options.export.constr || 'chart';\n constr = typeof Highcharts[constr] !== 'undefined' ? constr : 'chart';\n\n Highcharts[constr]('container', finalOptions, finalCallback);\n\n // Get the current global options\n const defaultOptions = getOptions();\n\n // Clear it just in case (e.g. the setOptions was used in the customCode)\n for (const prop in defaultOptions) {\n if (typeof defaultOptions[prop] !== 'function') {\n delete defaultOptions[prop];\n }\n }\n\n // Set the default options back\n setOptions(Highcharts.setOptionsObj);\n\n // Empty the custom global options object\n Highcharts.setOptionsObj = {};\n}\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { readFileSync } from 'fs';\nimport path from 'path';\n\nimport puppeteer from 'puppeteer';\n\nimport { getCachePath } from './cache.js';\nimport { getOptions } from './config.js';\nimport { setupHighcharts } from './highcharts.js';\nimport { log, logWithStack } from './logger.js';\nimport { __dirname } from './utils.js';\n\nimport ExportError from './errors/ExportError.js';\n\n// Get the template for the page\nconst template = readFileSync(__dirname + '/templates/template.html', 'utf8');\n\nlet browser;\n\n// Incremented every time the browser is lost. Pool workers are stamped with the\n// value current when they were created, which lets the pool tell that a worker\n// belongs to a browser that no longer exists. This is necessary because a page\n// belonging to a dead browser still reports isClosed() === false, so the page\n// itself cannot be asked whether it is usable.\nlet browserGeneration = 0;\n\n// The arguments the browser was last launched with, kept so that it can be\n// relaunched on the same terms after an unexpected disconnect.\nlet lastPuppeteerArgs = [];\n\n// Set while the browser is being deliberately closed, so that the resulting\n// disconnect is not mistaken for a crash.\nlet closingOnPurpose = false;\n\n// Shared promise for an in-flight launch, so that concurrent callers - the pool\n// creating several workers at once, typically - trigger a single launch rather\n// than one each.\nlet launchPromise = null;\n\n/**\n * Returns how long to keep retrying a browser launch before giving up.\n *\n * @returns {number} The retry window in milliseconds.\n */\nfunction getLaunchRetryWindow() {\n const configured = parseInt(getOptions().puppeteer?.launchRetryWindow);\n return isNaN(configured) || configured <= 0 ? 30000 : configured;\n}\n\n/**\n * Retrieves the existing Puppeteer browser instance.\n *\n * @returns {Promise} A Promise resolving to the Puppeteer browser\n * instance.\n *\n * @throws {ExportError} Throws an ExportError if no valid browser has been\n * created.\n */\nexport function get() {\n if (!browser) {\n throw new ExportError('[browser] No valid browser has been created.');\n }\n return browser;\n}\n\n/**\n * Returns the current browser generation. Pool workers are stamped with this\n * value on creation and compared against it on validation, so that workers\n * holding a page from a previous, now dead, browser can be identified and\n * replaced.\n *\n * @returns {number} The current browser generation.\n */\nexport function getGeneration() {\n return browserGeneration;\n}\n\n/**\n * Reports whether a usable browser is currently connected.\n *\n * @returns {boolean} True when a browser exists and is connected.\n */\nexport function isConnected() {\n return !!browser?.connected;\n}\n\n/**\n * Handles the browser disconnecting. Puppeteer emits this when the browser\n * process goes away for any reason, including being killed by an out of memory\n * reaper, which is the case this exists for.\n *\n * The browser reference is cleared so that the guard in create() will actually\n * relaunch it, and the generation is advanced so that every pool worker holding\n * a page from the dead browser fails validation and is replaced.\n */\nfunction handleDisconnect() {\n if (closingOnPurpose) {\n return;\n }\n\n browserGeneration++;\n browser = undefined;\n\n log(\n 1,\n `[browser] The browser disconnected unexpectedly. Invalidating all workers and relaunching on next use (generation ${browserGeneration}).`\n );\n}\n\n/**\n * Reports whether a process is still running, by asking the operating system\n * rather than trusting the child process object.\n *\n * NOTE: exitCode and signalCode are not dependable here. Both are null for a\n * running process, but either can be undefined depending on how the\n * process object was produced, and `undefined !== null` reads as exited -\n * which silently skips both the wait and the kill below, leaving the\n * process alive. Signal 0 performs the permission and existence checks\n * without delivering anything.\n *\n * @param {Object} proc - The child process to check.\n *\n * @returns {boolean} True while the process still exists.\n */\nfunction isAlive(proc) {\n if (!proc?.pid) {\n return false;\n }\n\n try {\n process.kill(proc.pid, 0);\n return true;\n } catch (error) {\n // ESRCH means no such process; EPERM means it exists but is not ours\n return error.code === 'EPERM';\n }\n}\n\n/**\n * Waits for a child process to exit, up to a limit.\n *\n * @param {Object} proc - The child process to wait for.\n * @param {number} timeout - How long to wait, in milliseconds.\n *\n * @returns {Promise} True if the process exited within the limit.\n */\nfunction waitForExit(proc, timeout) {\n return new Promise((resolve) => {\n if (!isAlive(proc)) {\n return resolve(true);\n }\n\n const onExit = () => {\n clearTimeout(timer);\n resolve(true);\n };\n\n const timer = setTimeout(() => {\n proc.removeListener('exit', onExit);\n resolve(false);\n }, timeout);\n\n proc.once('exit', onExit);\n });\n}\n\n/**\n * Closes a browser and makes certain its process has actually gone.\n *\n * @param {Object} instance - The Puppeteer browser instance to end.\n *\n * @returns {Promise} Resolves once the process has exited, or once giving\n * up waiting for it.\n */\nasync function terminate(instance) {\n // Take the process reference before closing, as it is not reachable afterwards\n const proc = instance.process();\n\n try {\n await instance.close();\n } catch (error) {\n logWithStack(2, error, '[browser] Could not cleanly close the browser.');\n }\n\n if (!proc) {\n log(\n 2,\n '[browser] No process handle for the browser, so its exit cannot be confirmed.'\n );\n return;\n }\n\n // NOTE: close() resolving does not mean the process has gone. It can resolve\n // when the connection drops, and the launch options deliberately disable\n // Puppeteer's signal handling, so nothing will clean up on our behalf.\n //\n // A surviving process keeps Chrome's lock on the user data directory,\n // which is shared by every launch here, so leaving one behind stops any\n // later browser from starting at all - and on shutdown it leaks a browser\n // process outright.\n if (await waitForExit(proc, 2000)) {\n return;\n }\n\n log(\n 2,\n '[browser] The browser process is still running after being closed, killing it.'\n );\n\n try {\n proc.kill('SIGKILL');\n } catch (error) {\n logWithStack(2, error, '[browser] Could not kill the browser process.');\n }\n\n if (isAlive(proc)) {\n log(\n 1,\n `[browser] The browser process ${proc.pid} has still not exited after being killed.`\n );\n }\n}\n\n/**\n * Creates a Puppeteer browser instance with the specified arguments.\n *\n * @param {Array} puppeteerArgs - Additional arguments for Puppeteer launch.\n *\n * @returns {Promise} A Promise resolving to the Puppeteer browser\n * instance.\n *\n * @throws {ExportError} Throws an ExportError if max retries to open a browser\n * instance are reached, or if no browser instance is found after retries.\n */\nexport async function create(puppeteerArgs) {\n // Remember the arguments so that an unexpected disconnect can relaunch the\n // browser on the same terms, and clear any deliberate-close state left from a\n // previous cycle\n if (puppeteerArgs !== undefined) {\n lastPuppeteerArgs = puppeteerArgs;\n }\n closingOnPurpose = false;\n\n if (browser?.connected) {\n return browser;\n }\n\n // NOTE: Concurrent callers must share a single launch. The pool creates\n // several workers at once, and after a disconnect each of them finds\n // no browser at the same moment - without this they would launch a\n // browser each.\n if (!launchPromise) {\n launchPromise = launchBrowser(lastPuppeteerArgs).finally(() => {\n launchPromise = null;\n });\n }\n\n return launchPromise;\n}\n\n/**\n * Launches a Puppeteer browser instance, retrying on failure.\n *\n * @param {Array} puppeteerArgs - Additional arguments for Puppeteer launch.\n *\n * @returns {Promise} A Promise resolving to the Puppeteer browser\n * instance.\n *\n * @throws {ExportError} Throws an ExportError if max retries to open a browser\n * instance are reached, or if no browser instance is found after retries.\n */\nasync function launchBrowser(puppeteerArgs) {\n // Get debug and other options\n const { puppeteer: puppeteerOptions, debug, other } = getOptions();\n\n // Get the debug options\n const { enable: enabledDebug, ...debugOptions } = debug;\n\n const launchOptions = {\n headless: other.browserShellMode ? 'shell' : true,\n userDataDir: puppeteerOptions.tempDir || './tmp/',\n args: puppeteerArgs,\n handleSIGINT: false,\n handleSIGTERM: false,\n handleSIGHUP: false,\n waitForInitialPage: false,\n defaultViewport: null,\n ...(enabledDebug && debugOptions)\n };\n\n // NOTE: Retry within a time budget rather than for a fixed number of attempts.\n // The previous 25 attempts four seconds apart meant a browser that could\n // never start took 100 seconds to say so, which is longer than an\n // orchestrator will usually wait before deciding the instance is\n // unhealthy - so it would be replaced while still reporting that it was\n // starting, and the real reason never surfaced.\n //\n // The delay grows and carries jitter, so that a fleet restarting together\n // does not retry in lockstep against whatever they are all contending for.\n const retryWindow = getLaunchRetryWindow();\n const deadline = Date.now() + retryWindow;\n\n let attempt = 0;\n let delay = 250;\n\n for (;;) {\n attempt++;\n\n try {\n log(\n 3,\n `[browser] Attempting to get a browser instance (attempt ${attempt}).`\n );\n browser = await puppeteer.launch(launchOptions);\n break;\n } catch (error) {\n const remaining = deadline - Date.now();\n\n if (remaining <= 0) {\n throw new ExportError(\n `[browser] Could not launch a browser within ${retryWindow}ms, after ${attempt} attempts.`\n ).setError(error);\n }\n\n // Not a full error yet, as Puppeteer sometimes needs a moment to settle\n logWithStack(\n 2,\n error,\n `[browser] Failed to launch a browser instance, retrying (attempt ${attempt}, ${Math.ceil(remaining / 1000)}s of the retry window left).`\n );\n\n await new Promise((resolve) =>\n setTimeout(\n resolve,\n Math.min(delay + Math.round(Math.random() * delay), remaining)\n )\n );\n\n delay = Math.min(delay * 2, 4000);\n }\n }\n\n // Shell mode inform\n if (launchOptions.headless === 'shell') {\n log(3, `[browser] Launched browser in shell mode.`);\n }\n\n // Debug mode inform\n if (enabledDebug) {\n log(3, `[browser] Launched browser in debug mode.`);\n }\n\n if (!browser) {\n throw new ExportError('[browser] Cannot find a browser to open.');\n }\n\n // Notice the browser going away, so that the pool's workers can be\n // invalidated and the browser relaunched, rather than the pool handing out\n // pages belonging to a process that no longer exists\n browser.once('disconnected', handleDisconnect);\n\n return browser;\n}\n\n/**\n * Closes the Puppeteer browser instance if it is connected.\n *\n * @returns {Promise} A Promise resolving to true after the browser\n * is closed.\n */\nexport async function close() {\n // Mark this as intentional so the resulting disconnect is not treated as a\n // crash and does not trigger a relaunch\n closingOnPurpose = true;\n\n // Close the browser and make sure its process has gone, so that shutdown does\n // not leave one behind holding the user data directory\n if (browser) {\n await terminate(browser);\n }\n\n browser = undefined;\n\n log(4, '[browser] Closed the browser.');\n}\n\n/**\n * Creates a new Puppeteer Page within an existing browser instance.\n *\n * If the browser instance is not available, returns false.\n *\n * The function creates a new page, disables caching, sets content using\n * setPageContent(), and returns the created Puppeteer Page.\n *\n * If the browser is not currently available it is relaunched first, so that the\n * pool can recover by itself after the browser process has gone away.\n *\n * @returns {(boolean|object)} Returns false if the browser instance is not\n * available, or a Puppeteer Page object representing the newly created page.\n */\nexport async function newPage() {\n // The browser may have gone away since the last page was made. Bring it back\n // rather than failing every export from here on - create() is guarded, so\n // concurrent callers share the one relaunch.\n if (!browser?.connected && !closingOnPurpose) {\n log(3, '[browser] No browser available, attempting to relaunch it.');\n await create();\n }\n\n if (!browser) {\n return false;\n }\n\n let page;\n\n try {\n // Create a page\n page = await browser.newPage();\n\n // Disable cache\n await page.setCacheEnabled(false);\n\n // Set the content\n await setPageContent(page);\n\n // Set page events\n setPageEvents(page);\n\n return page;\n } catch (error) {\n // NOTE: Without this, a page created above but failing any of the\n // subsequent steps is left open forever - the caller only receives\n // the error and never had a reference to close it. setPageContent is\n // the likely thrower, as it injects the entire Highcharts bundle and\n // so is sensitive to a CPU starved instance. That matters because on\n // a sustained create failure tarn retries every\n // createRetryInterval (200ms by default), which would leak another\n // browser tab on every attempt.\n if (page && !page.isClosed()) {\n try {\n await page.close();\n } catch (closeError) {\n logWithStack(\n 2,\n closeError,\n '[browser] Could not close a page that failed to be set up.'\n );\n }\n }\n\n throw error;\n }\n}\n\n/**\n * Clears the content of a Puppeteer Page based on the specified mode.\n *\n * @param {Object} page - The Puppeteer Page object to be cleared.\n * @param {boolean} hardReset - A flag indicating the type of clearing\n * to be performed. If true, navigates to 'about:blank' and resets content\n * and scripts. If false, clears the body content by setting a predefined HTML\n * structure.\n *\n * @throws {Error} Logs thrown error if clearing the page content fails.\n */\nexport async function clearPage(page, hardReset = false) {\n try {\n if (page && !page.isClosed()) {\n if (hardReset) {\n // Navigate to about:blank\n await page.goto('about:blank', { waitUntil: 'domcontentloaded' });\n\n // Set the content and and scripts again\n await setPageContent(page);\n } else {\n // Clear body content\n await page.evaluate(() => {\n document.body.innerHTML =\n '
';\n });\n }\n return true;\n }\n } catch (error) {\n logWithStack(\n 2,\n error,\n '[browser] Could not clear the content of the page.'\n );\n }\n\n return false;\n}\n\n/**\n * Adds custom JS and CSS resources to a Puppeteer Page based on the specified\n * options.\n *\n * @param {Object} page - The Puppeteer Page object to which resources will be\n * added.\n * @param {Object} options - All options and configuration.\n *\n * @returns {Promise>} - Promise resolving to an array of injected\n * resources.\n */\nexport async function addPageResources(page, options) {\n // Injected resources array\n const injectedResources = [];\n\n // Use resources\n const resources = options.customLogic.resources;\n if (resources) {\n const injectedJs = [];\n\n // Load custom JS code\n if (resources.js) {\n injectedJs.push({\n content: resources.js\n });\n }\n\n // Load scripts from all custom files\n if (resources.files) {\n for (const file of resources.files) {\n const isLocal = !file.startsWith('http') ? true : false;\n\n // Add each custom script from resources' files\n injectedJs.push(\n isLocal\n ? {\n content: readFileSync(file, 'utf8')\n }\n : {\n url: file\n }\n );\n }\n }\n\n for (const jsResource of injectedJs) {\n try {\n injectedResources.push(await page.addScriptTag(jsResource));\n } catch (error) {\n logWithStack(2, error, `[export] The JS resource cannot be loaded.`);\n }\n }\n injectedJs.length = 0;\n\n // Load CSS\n const injectedCss = [];\n if (resources.css) {\n let cssImports = resources.css.match(/@import\\s*([^;]*);/g);\n if (cssImports) {\n // Handle css section\n for (let cssImportPath of cssImports) {\n if (cssImportPath) {\n cssImportPath = cssImportPath\n .replace('url(', '')\n .replace('@import', '')\n .replace(/\"/g, '')\n .replace(/'/g, '')\n .replace(/;/, '')\n .replace(/\\)/g, '')\n .trim();\n\n // Add each custom css from resources\n if (cssImportPath.startsWith('http')) {\n injectedCss.push({\n url: cssImportPath\n });\n } else if (options.customLogic.allowFileResources) {\n injectedCss.push({\n path: path.join(__dirname, cssImportPath)\n });\n }\n }\n }\n }\n\n // The rest of the CSS section will be content by now\n injectedCss.push({\n content: resources.css.replace(/@import\\s*([^;]*);/g, '') || ' '\n });\n\n for (const cssResource of injectedCss) {\n try {\n injectedResources.push(await page.addStyleTag(cssResource));\n } catch (error) {\n logWithStack(2, error, `[export] The CSS resource cannot be loaded.`);\n }\n }\n injectedCss.length = 0;\n }\n }\n return injectedResources;\n}\n\n/**\n * Clears out all state set on the page with addScriptTag/addStyleTag. Removes\n * injected resources and resets CSS and script tags on the page. Additionally,\n * it destroys previously existing charts.\n *\n * @param {Object} page - The Puppeteer Page object from which resources will\n * be cleared.\n * @param {Array} injectedResources - Array of injected resources\n * to be cleared.\n */\nexport async function clearPageResources(page, injectedResources) {\n try {\n for (const resource of injectedResources) {\n await resource.dispose();\n }\n\n // Destroy old charts after export is done and reset all CSS and script tags\n await page.evaluate(() => {\n // We are not guaranteed that Highcharts is loaded, e,g, when doing SVG\n // exports\n if (typeof Highcharts !== 'undefined') {\n // eslint-disable-next-line no-undef\n const oldCharts = Highcharts.charts;\n\n // Check in any already existing charts\n if (Array.isArray(oldCharts) && oldCharts.length) {\n // Destroy old charts\n for (const oldChart of oldCharts) {\n oldChart && oldChart.destroy();\n // eslint-disable-next-line no-undef\n Highcharts.charts.shift();\n }\n }\n }\n\n const [...scriptsToRemove] = document.getElementsByTagName('script');\n const [, ...stylesToRemove] = document.getElementsByTagName('style');\n const [...linksToRemove] = document.getElementsByTagName('link');\n\n // Remove tags\n for (const element of [\n ...scriptsToRemove,\n ...stylesToRemove,\n ...linksToRemove\n ]) {\n element.remove();\n }\n });\n } catch (error) {\n logWithStack(2, error, `[browser] Could not clear page's resources.`);\n }\n}\n\n/**\n * Sets the content for a Puppeteer Page using a predefined template\n * and additional scripts. Also, sets the pageerror in order to catch\n * and display errors from the window context.\n *\n * @param {Object} page - The Puppeteer Page object for which the content\n * is being set.\n */\nasync function setPageContent(page) {\n await page.setContent(template, { waitUntil: 'domcontentloaded' });\n\n // Add all registered Higcharts scripts, quite demanding\n await page.addScriptTag({ path: `${getCachePath()}/sources.js` });\n\n // Set the initial animObject\n await page.evaluate(setupHighcharts);\n}\n\n/**\n * Set events for a Puppeteer Page.\n *\n * @param {Object} page - The Puppeteer Page object to set events to.\n */\nfunction setPageEvents(page) {\n // Get debug options\n const { debug } = getOptions();\n\n // Set the console listener, if needed\n if (debug.enable && debug.listenToConsole) {\n page.on('console', (message) => {\n console.log(`[debug] ${message.text()}`);\n });\n }\n\n // Set the pageerror listener\n page.on('pageerror', async (error) => {\n // It would seem like this may fire at the same time or shortly before\n // a page is closed.\n if (page.isClosed()) {\n return;\n }\n\n // TODO: Consider adding a switch here that turns on log(0) logging\n // on page errors.\n await page.$eval(\n '#container',\n (element, errorMessage) => {\n if (window._displayErrors) {\n element.innerHTML = errorMessage;\n }\n },\n `

Chart input data error:

${error.toString()}`\n );\n });\n}\n\nexport default {\n get,\n getGeneration,\n isConnected,\n create,\n close,\n newPage,\n clearPage,\n addPageResources,\n clearPageResources\n};\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { addPageResources, clearPageResources } from './browser.js';\nimport { getCache } from './cache.js';\nimport { triggerExport } from './highcharts.js';\nimport { log } from './logger.js';\n\nimport svgTemplate from './../templates/svg_export/svg_export.js';\n\nimport ExportError from './errors/ExportError.js';\n\n/**\n * Retrieves the clipping region coordinates of the specified page element with\n * the id 'chart-container'.\n *\n * @param {Object} page - Puppeteer page object.\n *\n * @returns {Promise} Promise resolving to an object containing\n * x, y, width, and height properties.\n */\nconst getClipRegion = (page) =>\n page.$eval('#chart-container', (element) => {\n const { x, y, width, height } = element.getBoundingClientRect();\n return {\n x,\n y,\n width,\n height: Math.trunc(height > 1 ? height : 500)\n };\n });\n\n/**\n * Creates an image using Puppeteer's page screenshot functionality with\n * specified options.\n *\n * @param {Object} page - Puppeteer page object.\n * @param {string} type - Image type.\n * @param {string} encoding - Image encoding.\n * @param {Object} clip - Clipping region coordinates.\n * @param {number} rasterizationTimeout - Timeout for rasterization\n * in milliseconds.\n *\n * @returns {Promise} Promise resolving to the image buffer or rejecting\n * with an ExportError for timeout.\n */\nconst createImage = async (\n page,\n type,\n encoding,\n clip,\n rasterizationTimeout\n) => {\n let timer;\n\n try {\n return await Promise.race([\n page.screenshot({\n type,\n encoding,\n clip,\n captureBeyondViewport: true,\n fullPage: false,\n optimizeForSpeed: true,\n ...(type !== 'png' ? { quality: 80 } : {}),\n\n // #447, #463 - always render on a transparent page if the expected type\n // format is PNG\n omitBackground: type == 'png'\n }),\n new Promise((_resolve, reject) => {\n timer = setTimeout(\n () => reject(new ExportError('Rasterization timeout')),\n rasterizationTimeout || 1500\n );\n })\n ]);\n } finally {\n // The timer outlives the race otherwise, keeping itself and everything its\n // callback closes over alive for the rest of the timeout on every successful\n // export\n clearTimeout(timer);\n }\n};\n\n/**\n * Creates a PDF using Puppeteer's page pdf functionality with specified\n * options.\n *\n * @param {Object} page - Puppeteer page object.\n * @param {number} height - PDF height.\n * @param {number} width - PDF width.\n * @param {string} encoding - PDF encoding.\n *\n * @returns {Promise} Promise resolving to the PDF buffer.\n */\nconst createPDF = async (\n page,\n height,\n width,\n encoding,\n rasterizationTimeout\n) => {\n await page.emulateMediaType('screen');\n\n return page.pdf({\n // This will remove an extra empty page in PDF exports\n height: height + 1,\n width,\n encoding,\n timeout: rasterizationTimeout || 1500\n });\n};\n\n/**\n * Creates an SVG string by evaluating the outerHTML of the first 'svg' element\n * inside an element with the id 'container'.\n *\n * @param {Object} page - Puppeteer page object.\n *\n * @returns {Promise} Promise resolving to the SVG string.\n */\nconst createSVG = (page) =>\n page.$eval('#container svg:first-of-type', (element) => element.outerHTML);\n\n/**\n * Sets the specified chart and options as configuration into the triggerExport\n * function within the window context using page.evaluate.\n *\n * @param {Object} page - Puppeteer page object.\n * @param {any} chart - The chart object to be configured.\n * @param {Object} options - Configuration options for the chart.\n *\n * @returns {Promise} Promise resolving after the configuration is set.\n */\nconst setAsConfig = async (page, chart, options, displayErrors) => {\n // Get rid of the redunant string data\n options.export.instr = null;\n options.export.infile = null;\n\n // Get the size of the export input\n const totalSize = Buffer.byteLength(\n options.export?.strInj ? options.export?.strInj : JSON.stringify(chart),\n 'utf-8'\n );\n\n // Log the size in MB\n log(\n 4,\n `[export] The current total size of data passed to a page is around ${(\n totalSize /\n (1024 * 1024)\n ).toFixed(2)} MB`\n );\n\n // Check the size of data passed to the page\n if (totalSize >= 100 * 1024 * 1024) {\n throw new ExportError(`[export] The data passed to a page exceeded 100MB.`);\n }\n\n // Trigger the Highcharts chart creation\n return page.evaluate(triggerExport, chart, options, displayErrors);\n};\n\n/**\n * Exports to a chart from a page using Puppeteer.\n *\n * @param {Object} page - Puppeteer page object.\n * @param {any} chart - The chart object or SVG configuration to be exported.\n * @param {Object} options - Export options and configuration.\n *\n * @returns {Promise} Promise resolving to\n * the exported data or rejecting with an ExportError.\n */\nexport default async (page, chart, options) => {\n // Injected resources array (additional JS and CSS)\n let injectedResources = [];\n\n try {\n log(4, '[export] Determining export path.');\n\n const exportOptions = options.export;\n\n // Decide whether display error or debbuger wrapper around it\n const displayErrors =\n exportOptions?.options?.chart?.displayErrors &&\n getCache().activeManifest.modules.debugger;\n\n let isSVG;\n if (\n chart.indexOf &&\n (chart.indexOf('= 0 || chart.indexOf('= 0)\n ) {\n // SVG input handling\n log(4, '[export] Treating as SVG.');\n\n // If input is also SVG, just return it\n if (exportOptions.type === 'svg') {\n return chart;\n }\n\n isSVG = true;\n await page.setContent(svgTemplate(chart), {\n waitUntil: 'domcontentloaded'\n });\n } else {\n // JSON config handling\n log(4, '[export] Treating as config.');\n\n // Need to perform straight inject\n if (exportOptions.strInj) {\n // Injection based configuration export\n await setAsConfig(\n page,\n {\n chart: {\n height: exportOptions.height,\n width: exportOptions.width\n }\n },\n options,\n displayErrors\n );\n } else {\n // Basic configuration export\n chart.chart.height = exportOptions.height;\n chart.chart.width = exportOptions.width;\n\n await setAsConfig(page, chart, options, displayErrors);\n }\n }\n\n // Keeps track of all resources added on the page with addXXXTag. etc\n // It's VITAL that all added resources ends up here so we can clear things\n // out when doing a new export in the same page!\n injectedResources = await addPageResources(page, options);\n\n // Get the real chart size and set the zoom accordingly\n const size = isSVG\n ? await page.evaluate((scale) => {\n const svgElement = document.querySelector(\n '#chart-container svg:first-of-type'\n );\n\n // Get the values correctly scaled\n const chartHeight = svgElement.height.baseVal.value * scale;\n const chartWidth = svgElement.width.baseVal.value * scale;\n\n // In case of SVG the zoom must be set directly for body\n // Set the zoom as scale\n document.body.style.zoom = scale;\n\n // Set the margin to 0px\n document.body.style.margin = '0px';\n\n return {\n chartHeight,\n chartWidth\n };\n }, parseFloat(exportOptions.scale))\n : await page.evaluate(() => {\n const { chartHeight, chartWidth } = window.Highcharts.charts[0];\n\n // No need for such scale manipulation in case of other types of exports\n // Reset the zoom for other exports than to SVGs\n document.body.style.zoom = 1;\n\n return {\n chartHeight,\n chartWidth\n };\n });\n\n // Set final height and width for viewport\n const viewportHeight = Math.abs(\n Math.ceil(size.chartHeight || exportOptions.height)\n );\n const viewportWidth = Math.abs(\n Math.ceil(size.chartWidth || exportOptions.width)\n );\n\n // Get the clip region for the page\n const { x, y } = await getClipRegion(page);\n\n // Set the final viewport now that we have the real height\n await page.setViewport({\n height: viewportHeight,\n width: viewportWidth,\n deviceScaleFactor: isSVG ? 1 : parseFloat(exportOptions.scale)\n });\n\n let data;\n // Rasterization process\n if (exportOptions.type === 'svg') {\n // SVG\n data = await createSVG(page);\n } else if (['png', 'jpeg'].includes(exportOptions.type)) {\n // PNG or JPEG\n data = await createImage(\n page,\n exportOptions.type,\n 'base64',\n {\n width: viewportWidth,\n height: viewportHeight,\n x,\n y\n },\n exportOptions.rasterizationTimeout\n );\n } else if (exportOptions.type === 'pdf') {\n // PDF\n data = await createPDF(\n page,\n viewportHeight,\n viewportWidth,\n 'base64',\n exportOptions.rasterizationTimeout\n );\n } else {\n throw new ExportError(\n `[export] Unsupported output format ${exportOptions.type}.`\n );\n }\n\n // Clear previously injected JS and CSS resources\n await clearPageResources(page, injectedResources);\n return data;\n } catch (error) {\n await clearPageResources(page, injectedResources);\n return error;\n }\n};\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport cssTemplate from './css.js';\n\nexport default (chart) => `\n\n\n \n \n Highcharts Export\n \n \n \n
\n ${chart}\n
\n \n\n\n`;\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\n/**\n * Machine readable error codes, returned as the `errorCode` property of an\n * error response.\n *\n * These exist because the HTTP status code cannot carry the distinction that\n * matters most in production: a request refused because the server was busy and\n * a request refused because it was malformed are both reported as 400. Without a\n * code, a dashboard cannot separate a capacity problem from callers sending bad\n * data, and a client cannot tell whether retrying is worthwhile.\n *\n * Treat these as a stable contract - callers may branch on them.\n */\nexport const errorCodes = {\n // The request itself was not usable: missing body, no chart data, or content\n // that is not allowed. Retrying without changing the request will not help.\n INVALID_REQUEST: 'EXPORT_INVALID_REQUEST',\n\n // The server was already holding as many queued exports as it is willing to,\n // and refused this one without starting work on it. Retrying later, ideally\n // with backoff, is appropriate.\n QUEUE_FULL: 'EXPORT_QUEUE_FULL',\n\n // No worker became available within the acquire timeout. Same meaning for a\n // caller as QUEUE_FULL, but reached by waiting rather than by being refused\n // up front.\n ACQUIRE_TIMEOUT: 'EXPORT_ACQUIRE_TIMEOUT',\n\n // The chart was too large or complex to render within the allotted time.\n RASTERIZATION_TIMEOUT: 'EXPORT_RASTERIZATION_TIMEOUT',\n\n // The client disconnected before the export could be served, so the work was\n // discarded. Never reaches a caller by definition - it exists so that\n // abandoned work is distinguishable in logs and counters from work that\n // genuinely failed.\n CLIENT_GONE: 'EXPORT_CLIENT_GONE',\n\n // The export failed for a reason that is not one of the above.\n EXPORT_FAILED: 'EXPORT_FAILED'\n};\n\nexport default errorCodes;\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { Pool } from 'tarn';\nimport { v4 as uuid } from 'uuid';\n\nimport {\n create as createBrowser,\n close as closeBrowser,\n newPage,\n clearPage,\n getGeneration as getBrowserGeneration\n} from './browser.js';\nimport puppeteerExport from './export.js';\nimport { log, logWithStack } from './logger.js';\nimport { measureTime } from './utils.js';\n\nimport ExportError from './errors/ExportError.js';\nimport { errorCodes } from './errors/codes.js';\n\n// The pool instance\nlet pool = false;\n\n// Pool statistics\nexport const stats = {\n performedExports: 0,\n exportAttempts: 0,\n exportFromSvgAttempts: 0,\n timeSpent: 0,\n droppedExports: 0,\n spentAverage: 0,\n rejectedForCapacity: 0,\n abandonedExports: 0\n};\n\nlet poolConfig = {};\n\n// The resolved maximum number of exports allowed to wait for a worker\nlet queueLimit = 0;\n\n// Worker creations that have failed in a row, reset by any success. Used to stop\n// the pool retrying against a browser that is not going to start working again.\nlet consecutiveCreateFailures = 0;\n\n/**\n * Returns the number of worker creations that have failed in a row.\n *\n * @returns {number} The current consecutive failure count.\n */\nexport const getConsecutiveCreateFailures = () => consecutiveCreateFailures;\n\n/**\n * Resolves the queue limit from the pool configuration.\n *\n * A limit of 0 means derive it from the pool size. Four times maxWorkers keeps\n * the worst case wait at roughly four exports' worth of time, which is a\n * meaningful bound, while leaving enough slack to absorb normal bursts.\n *\n * @param {Object} config - The pool section of the configuration.\n *\n * @returns {number} The resolved queue limit.\n */\nconst resolveQueueLimit = (config) => {\n const configured = parseInt(config.queueLimit);\n const maxWorkers = parseInt(config.maxWorkers);\n\n if (!isNaN(configured) && configured > 0) {\n return configured;\n }\n\n return (isNaN(maxWorkers) ? 8 : maxWorkers) * 4;\n};\n\n/**\n * Returns the number of exports currently allowed to wait for a worker.\n *\n * @returns {number} The active queue limit.\n */\nexport const getQueueLimit = () => queueLimit;\n\n/**\n * Returns how long to wait before answering a request refused because the queue\n * was full.\n *\n * @returns {number} The delay in milliseconds.\n */\nexport const getQueueRejectDelay = () => {\n const delay = parseInt(poolConfig.queueRejectDelay);\n return isNaN(delay) || delay < 0 ? 0 : delay;\n};\n\nconst factory = {\n /**\n * Creates a new worker page for the export pool.\n *\n * @returns {Object} - An object containing the worker ID, a reference to the\n * browser page, and initial work count.\n *\n * @throws {ExportError} - If there's an error during the creation of the new\n * page.\n */\n create: async () => {\n let page = false;\n\n const id = uuid();\n const startDate = new Date().getTime();\n\n try {\n page = await newPage();\n\n if (!page || page.isClosed()) {\n throw new ExportError('The page is invalid or closed.');\n }\n\n consecutiveCreateFailures = 0;\n\n log(\n 3,\n `[pool] Successfully created a worker ${id} - took ${\n new Date().getTime() - startDate\n } ms.`\n );\n } catch (error) {\n ++consecutiveCreateFailures;\n\n throw new ExportError(\n 'Error encountered when creating a new page.'\n ).setError(error);\n }\n\n return {\n id,\n page,\n // Which browser this page belongs to, so that it can be recognised as\n // stale if that browser goes away - see factory.validate\n generation: getBrowserGeneration(),\n // Try to distribute the initial work count\n workCount: Math.round(Math.random() * (poolConfig.workLimit / 2))\n };\n },\n\n /**\n * Validates a worker page in the export pool, checking if it has exceeded\n * the work limit.\n *\n * @param {Object} workerHandle - The handle to the worker, containing the\n * worker's ID, a reference to the browser page, and work count.\n *\n * @returns {boolean} - Returns true if the worker is valid and within\n * the work limit; otherwise, returns false.\n */\n validate: async (workerHandle) => {\n // NOTE: In certain cases acquiring throws a TargetCloseError, which may\n // be caused by two things:\n // - The page is closed and attempted to be reused.\n // - Lost contact with the browser\n // What we're seeing in logs is that successive exports typically\n // succeeds, and the server recovers, indicating that it's likely\n // the first case. This is an attempt at allievating the issue by\n // simply not validating the worker if the page is null or closed.\n //\n // The actual result from when this happened, was that a worker would\n // be completely locked, stopping it from being acquired until\n // its work count reached the limit.\n if (!workerHandle.page || workerHandle.page?.isClosed()) {\n return false;\n }\n\n // Set when an export left the page in a state it cannot be reused from - see\n // the rasterization timeout handling in postWork\n if (workerHandle.mustRecycle) {\n log(\n 3,\n `[pool] Worker failed validation: its page was left unusable by a previous export.`\n );\n return false;\n }\n\n // NOTE: A page whose browser has gone away still reports\n // isClosed() === false, so the check above cannot detect it and the\n // pool would keep handing out pages belonging to a dead process,\n // failing every export while continuing to report healthy workers.\n //\n // The generation is the reliable signal: it advances whenever the\n // browser is lost, so any worker created against an earlier one is\n // stale. Returning false here makes tarn destroy the worker and\n // create a replacement, which relaunches the browser via newPage.\n if (workerHandle.generation !== getBrowserGeneration()) {\n log(\n 3,\n `[pool] Worker failed validation: it belongs to a browser that is gone (worker generation ${workerHandle.generation}, current ${getBrowserGeneration()}).`\n );\n return false;\n }\n\n // NOTE: Wait for the page clearing started when this worker was released\n // to complete before handing it out again.\n //\n // Tarn's release() is synchronous: it invokes the 'release' event\n // handlers and then returns the resource to the free list within the\n // same tick, without awaiting anything. Clearing the page in that\n // handler therefore overlaps the next export whenever an acquire is\n // already waiting, which is exactly the case under saturation.\n //\n // In practice the visible damage is currently limited, because\n // export.js already calls clearPageResources at the end of every\n // export, which destroys the old charts and removes the injected\n // tags. The innerHTML reset done here is a second pass. But that\n // makes correctness depend on the ordering of two independent cleanup\n // paths, and an innerHTML reset landing part way through the next\n // render would wipe its container.\n //\n // Tarn does await validate, so this is the point where the clearing\n // is guaranteed to have finished. Doing it here rather than in the\n // handler keeps the work overlapped with the worker's idle time, and\n // lets a page that could not be cleared recycle the worker instead of\n // being exported onto.\n if (workerHandle.cleanPromise) {\n const cleared = await workerHandle.cleanPromise;\n workerHandle.cleanPromise = null;\n\n if (!cleared) {\n log(\n 3,\n `[pool] Worker failed validation: the page could not be cleared after its previous export.`\n );\n return false;\n }\n }\n\n if (\n poolConfig.workLimit &&\n ++workerHandle.workCount > poolConfig.workLimit\n ) {\n log(\n 3,\n `[pool] Worker failed validation: exceeded work limit (limit is ${poolConfig.workLimit}).`\n );\n return false;\n }\n return true;\n },\n\n /**\n * Destroys a worker entry in the export pool, closing its associated page.\n *\n * @param {Object} workerHandle - The handle to the worker, containing\n * the worker's ID and a reference to the browser page.\n */\n destroy: async (workerHandle) => {\n log(3, `[pool] Destroying pool entry ${workerHandle.id}.`);\n\n if (workerHandle.page && !workerHandle.page.isClosed()) {\n await workerHandle.page.close();\n }\n }\n\n // log: (message, level) => log(1, '[tarn] ' + message)\n};\n\n/**\n * Initializes the export pool with the provided configuration, creating\n * a browser instance and setting up worker resources.\n *\n * @param {Object} config - Configuration options for the export pool along\n * with custom puppeteer arguments for the puppeteer.launch function.\n */\nexport const initPool = async (config) => {\n // For the module scope usage\n poolConfig = config && config.pool ? { ...config.pool } : {};\n\n // Work out how deep the queue of waiting exports is allowed to get\n queueLimit = resolveQueueLimit(poolConfig);\n\n // Create a browser instance with the puppeteer arguments\n await createBrowser(config.puppeteerArgs);\n\n log(\n 3,\n `[pool] Initializing pool with workers: min ${poolConfig.minWorkers}, max ${poolConfig.maxWorkers}.`\n );\n\n if (pool) {\n return log(\n 4,\n '[pool] Already initialized, please kill it before creating a new one.'\n );\n }\n\n if (parseInt(poolConfig.minWorkers) > parseInt(poolConfig.maxWorkers)) {\n poolConfig.minWorkers = poolConfig.maxWorkers;\n }\n\n try {\n // Create a pool along with a minimal number of resources\n pool = new Pool({\n // Get the create/validate/destroy/log functions\n ...factory,\n min: parseInt(poolConfig.minWorkers),\n max: parseInt(poolConfig.maxWorkers),\n acquireTimeoutMillis: poolConfig.acquireTimeout,\n createTimeoutMillis: poolConfig.createTimeout,\n destroyTimeoutMillis: poolConfig.destroyTimeout,\n idleTimeoutMillis: poolConfig.idleTimeout,\n createRetryIntervalMillis: poolConfig.createRetryInterval,\n reapIntervalMillis: poolConfig.reaperInterval,\n propagateCreateError: false\n });\n\n // Set events\n pool.on('release', (resource) => {\n // Start clearing the page, but deliberately do not await it here - see\n // the note in factory.validate, which is where the result is awaited\n // before the worker can be handed out again. The catch is belt and braces:\n // clearPage resolves false rather than rejecting, and nothing must be\n // able to turn this into an unhandled rejection.\n resource.cleanPromise = clearPage(resource.page, false).catch(\n () => false\n );\n\n log(4, `[pool] Releasing a worker with ID ${resource.id}.`);\n });\n\n pool.on('destroySuccess', (eventId, resource) => {\n log(4, `[pool] Destroyed a worker with ID ${resource.id}.`);\n resource.page = null;\n });\n\n const initialResources = [];\n // Create an initial number of resources\n for (let i = 0; i < poolConfig.minWorkers; i++) {\n try {\n const resource = await pool.acquire().promise;\n initialResources.push(resource);\n } catch (error) {\n logWithStack(2, error, '[pool] Could not create an initial resource.');\n }\n }\n\n // Release the initial number of resources back to the pool\n initialResources.forEach((resource) => {\n pool.release(resource);\n });\n\n log(\n 3,\n `[pool] The pool is ready${initialResources.length ? ` with ${initialResources.length} initial resources waiting.` : '.'}`\n );\n } catch (error) {\n throw new ExportError(\n '[pool] Could not create the pool of workers.'\n ).setError(error);\n }\n};\n\n/**\n * Kills all workers in the pool, destroys the pool, and closes the browser\n * instance.\n *\n * @returns {Promise} A promise that resolves after the workers are\n * killed, the pool is destroyed, and the browser is closed.\n */\nexport async function killPool() {\n log(3, '[pool] Killing pool with all workers and closing browser.');\n\n // If still alive, destroy the pool of pages before closing a browser\n if (pool) {\n // Free up not released workers\n for (const worker of pool.used) {\n pool.release(worker.resource);\n }\n\n // Destroy the pool if it is still available\n if (!pool.destroyed) {\n await pool.destroy();\n log(4, '[browser] Destroyed the pool of resources.');\n }\n }\n\n // Close the browser instance\n await closeBrowser();\n}\n\n/**\n * Processes the export work using a worker from the pool. Acquires a worker\n * handle from the pool, performs the export using puppeteer, and releases\n * the worker handle back to the pool.\n *\n * @param {string} chart - The chart data or configuration to be exported.\n * @param {Object} options - Export options and configuration.\n *\n * @returns {Promise} A promise that resolves with the export resultand\n * options.\n *\n * @throws {ExportError} If an error occurs during the export process.\n */\nexport const postWork = async (chart, options) => {\n let workerHandle;\n\n try {\n log(4, '[pool] Work received, starting to process.');\n\n ++stats.exportAttempts;\n if (poolConfig.benchmarking) {\n getPoolInfo();\n }\n\n if (!pool) {\n throw new ExportError('Work received, but pool has not been started.');\n }\n\n // NOTE: Refuse the work rather than queue it when the queue is already at\n // its limit.\n //\n // Throughput does not improve past the pool size - measured, it peaks\n // at roughly the number of workers and then falls - so an unbounded\n // queue cannot buy capacity. What it does buy is latency and memory:\n // every waiting request holds its parsed body until it is served or\n // times out, and a saturated server was observed accepting more than\n // twenty times the work it could complete, queueing thousands of\n // requests and then failing most of them on timeout.\n //\n // Failing here is cheap and immediate, and carries a distinct error\n // code so that a caller and a dashboard can tell this apart from a\n // malformed request.\n if (pool.numPendingAcquires() >= queueLimit) {\n ++stats.rejectedForCapacity;\n\n throw new ExportError(\n (options.payload?.requestId\n ? `For request with ID ${options.payload?.requestId} - `\n : '') +\n `The server is at capacity: ${pool.numPendingAcquires()} exports are already waiting for a worker (limit is ${queueLimit}). Please retry shortly.`\n ).setCode(errorCodes.QUEUE_FULL);\n }\n\n // NOTE: Drop work whose client has already gone, before it takes a queue\n // slot or a worker.\n //\n // Measured on the unbounded queue: 60 clients that each gave up after\n // 150ms left 1367 exports still queued a second after the last client\n // had disconnected, and the pool spent a further five seconds\n // rendering charts nobody would receive. Under a retry storm that\n // compounds - every retry adds work while the abandoned original is\n // still being rendered - so capacity falls with each round.\n const abortSignal = options.payload?.abortSignal;\n\n if (abortSignal?.aborted) {\n ++stats.abandonedExports;\n\n throw new ExportError(\n (options.payload?.requestId\n ? `For request with ID ${options.payload?.requestId} - `\n : '') + 'The client disconnected before a worker was available.'\n ).setCode(errorCodes.CLIENT_GONE);\n }\n\n // Acquire the worker along with the id of resource and work count\n const acquireCounter = measureTime();\n try {\n log(4, '[pool] Acquiring a worker handle.');\n workerHandle = await pool.acquire().promise;\n\n // Check the page acquire time\n if (options.server.benchmarking) {\n log(\n 5,\n options.payload?.requestId\n ? `[benchmark] Request with ID ${options.payload?.requestId} -`\n : '[benchmark]',\n `Acquired a worker handle: ${acquireCounter()}ms.`\n );\n }\n } catch (error) {\n throw new ExportError(\n (options.payload?.requestId\n ? `For request with ID ${options.payload?.requestId} - `\n : '') +\n `Error encountered when acquiring an available entry: ${acquireCounter()}ms.`\n )\n .setCode(errorCodes.ACQUIRE_TIMEOUT)\n .setError(error);\n }\n log(4, '[pool] Acquired a worker handle.');\n\n // The client may have gone while this request was queued. Hand the worker\n // straight back rather than spending it on a result nobody will read.\n if (abortSignal?.aborted) {\n ++stats.abandonedExports;\n pool.release(workerHandle);\n workerHandle = null;\n\n throw new ExportError(\n (options.payload?.requestId\n ? `For request with ID ${options.payload?.requestId} - `\n : '') + 'The client disconnected while waiting for a worker.'\n ).setCode(errorCodes.CLIENT_GONE);\n }\n\n if (!workerHandle.page) {\n throw new ExportError(\n 'Resolved worker page is invalid: the pool setup is wonky.'\n );\n }\n\n // Save the start time\n let workStart = new Date().getTime();\n\n log(4, `[pool] Starting work on pool entry with ID ${workerHandle.id}.`);\n\n // Perform an export on a puppeteer level\n const exportCounter = measureTime();\n const result = await puppeteerExport(workerHandle.page, chart, options);\n\n // Check if it's an error\n if (result instanceof Error) {\n // NOTE: If there's a rasterization timeout, we want need to flush the page.\n // This is because the page may be in a state where it's waiting for\n // the screenshot to finish even though the timeout has occured.\n // Which of course causes a lot of issues with the event system,\n // and page consistency.\n //\n // NOTE: Only page.screenshot will throw this, timeouts for PDF's are\n // handled by the page.pdf function itself.\n //\n // ...yes, this is ugly.\n if (result.message === 'Rasterization timeout') {\n // NOTE: This worker must not serve another export. Its page may still be\n // waiting on the screenshot that timed out, so its state is unknown.\n //\n // Marking it rather than discarding the page reference is the point\n // here. Setting page to null used to be how this was done, but\n // factory.destroy only closes a page it can still see, so nulling\n // the reference meant the page was never closed - and with a process\n // per tab, every timeout leaked a renderer process. Under load that\n // compounds: the leaked processes take CPU from the exports still\n // running, which makes more of them time out.\n workerHandle.workCount = poolConfig.workLimit + 1;\n workerHandle.mustRecycle = true;\n }\n\n if (\n result.name === 'TimeoutError' ||\n result.message === 'Rasterization timeout'\n ) {\n throw new ExportError(\n 'Rasterization timeout: your chart may be too complex or large, and failed to render within the allotted time.'\n )\n .setCode(errorCodes.RASTERIZATION_TIMEOUT)\n .setError(result);\n } else {\n throw new ExportError(\n (options.payload?.requestId\n ? `For request with ID ${options.payload?.requestId} - `\n : '') + `Error encountered during export: ${exportCounter()}ms.`\n )\n .setCode(errorCodes.EXPORT_FAILED)\n .setError(result);\n }\n }\n\n // Check the Puppeteer export time\n if (options.server.benchmarking) {\n log(\n 5,\n options.payload?.requestId\n ? `[benchmark] Request with ID ${options.payload?.requestId} -`\n : '[benchmark]',\n `Exported a chart sucessfully: ${exportCounter()}ms.`\n );\n }\n\n // Release the resource back to the pool\n pool.release(workerHandle);\n\n // Used for statistics in averageTime and processedWorkCount, which\n // in turn is used by the /health route.\n const workEnd = new Date().getTime();\n const exportTime = workEnd - workStart;\n stats.timeSpent += exportTime;\n stats.spentAverage = stats.timeSpent / ++stats.performedExports;\n\n log(4, `[pool] Work completed in ${exportTime} ms.`);\n\n // Otherwise return the result\n return {\n result,\n options\n };\n } catch (error) {\n // NOTE: Work discarded because its client left is not a failure of the\n // server, so it is counted separately rather than inflating\n // droppedExports and depressing the success ratio that /health\n // reports. It has its own counter, incremented where it is detected.\n if (error.errorCode !== errorCodes.CLIENT_GONE) {\n ++stats.droppedExports;\n }\n\n if (workerHandle) {\n pool.release(workerHandle);\n }\n\n throw new ExportError(`[pool] In pool.postWork: ${error.message}`).setError(\n error\n );\n }\n};\n\n/**\n * Retrieves the current pool instance.\n *\n * @returns {Object|null} The current pool instance if initialized, or null\n * if the pool has not been created.\n */\nexport const getPool = () => pool;\n\n/**\n * Retrieves pool information in JSON format, including minimum and maximum\n * workers, available workers, workers in use, and pending acquire requests.\n *\n * @returns {Object} Pool information in JSON format.\n */\nexport const getPoolInfoJSON = () => ({\n min: pool.min,\n max: pool.max,\n all: pool.numFree() + pool.numUsed(),\n available: pool.numFree(),\n used: pool.numUsed(),\n pending: pool.numPendingAcquires(),\n queueLimit\n});\n\n/**\n * Logs information about the current state of the pool, including the minimum\n * and maximum workers, available workers, workers in use, and pending acquire\n * requests.\n */\nexport function getPoolInfo() {\n const { min, max, all, available, used, pending } = getPoolInfoJSON();\n\n log(5, `[pool] The minimum number of resources allowed by pool: ${min}.`);\n log(5, `[pool] The maximum number of resources allowed by pool: ${max}.`);\n log(5, `[pool] The number of all created resources: ${all}.`);\n log(5, `[pool] The number of available resources: ${available}.`);\n log(5, `[pool] The number of acquired resources: ${used}.`);\n log(5, `[pool] The number of resources waiting to be acquired: ${pending}.`);\n}\n\nexport default {\n initPool,\n killPool,\n postWork,\n getPool,\n getPoolInfo,\n getPoolInfoJSON,\n getQueueLimit,\n getConsecutiveCreateFailures,\n getStats: () => stats\n};\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\n/**\n * @overview Used to sanitize the strings coming from the exporting module\n * to prevent XSS attacks (with the DOMPurify library).\n **/\n\nimport { JSDOM } from 'jsdom';\nimport DOMPurify from 'dompurify';\n\nimport { envs } from './envs.js';\n\n// The purifier, built on first use and then reused.\n//\n// NOTE: Building a DOM and a purifier per call is by far the most expensive part\n// of sanitizing, and this runs on every SVG export. It is also synchronous,\n// so the cost is paid on the event loop and delays every other request in\n// flight, not just this one.\n//\n// Only the instance is shared. The options stay per call, since FORBID_ATTR\n// depends on configuration that can be read at any time, and DOMPurify\n// applies the options it is given on each call.\nlet purifier;\n\n/**\n * Returns the shared purifier, building it if this is the first call.\n *\n * @returns {Object} The DOMPurify instance.\n */\nfunction getPurifier() {\n if (!purifier) {\n purifier = DOMPurify(new JSDOM('').window);\n }\n\n return purifier;\n}\n\n/**\n * Sanitizes a given HTML string by removing tags and any content within them.\n *\n * @param {string} input The HTML string to be sanitized.\n * @returns {string} The sanitized HTML string.\n */\nexport function sanitize(input) {\n const forbidden = [];\n\n if (!envs.OTHER_ALLOW_XLINK) {\n forbidden.push('xlink:href');\n }\n\n return getPurifier().sanitize(input, {\n ADD_TAGS: ['foreignObject'],\n FORBID_ATTR: forbidden,\n HTML_INTEGRATION_POINTS: { foreignobject: true }\n });\n}\n\nexport default sanitize;\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { readFileSync, writeFileSync } from 'fs';\n\nimport { getOptions, initExportSettings } from './config.js';\nimport { log, logWithStack } from './logger.js';\nimport { killPool, postWork, stats } from './pool.js';\nimport {\n fixType,\n handleResources,\n isCorrectJSON,\n optionsStringify,\n roundNumber,\n toBoolean,\n wrapAround\n} from './utils.js';\nimport { sanitize } from './sanitize.js';\nimport ExportError from './errors/ExportError.js';\n\nlet allowCodeExecution = false;\n\n/**\n * Starts an export process. The `settings` contains final options gathered\n * from all possible sources (config, env, cli, json). The `endCallback` is\n * called when the export is completed, with an error object as the first\n * argument and the second containing the base64 respresentation of a chart.\n *\n * @param {Object} settings - The settings object containing export\n * configuration.\n * @param {function} endCallback - The callback function to be invoked upon\n * finalizing work or upon error occurance of the exporting process.\n *\n * @returns {void} This function does not return a value directly; instead,\n * it communicates results via the endCallback.\n */\nexport const startExport = async (settings, endCallback) => {\n // Starting exporting process message\n log(4, '[chart] Starting the exporting process.');\n\n // Initialize options\n const options = initExportSettings(settings, getOptions());\n\n // Get the export options\n const exportOptions = options.export;\n\n // If SVG is an input (argument can be sent only by the request)\n if (options.payload?.svg && options.payload.svg !== '') {\n try {\n log(4, '[chart] Attempting to export from a SVG input.');\n\n const result = exportAsString(\n sanitize(options.payload.svg), // #209\n options,\n endCallback\n );\n\n ++stats.exportFromSvgAttempts;\n return result;\n } catch (error) {\n return endCallback(\n new ExportError('[chart] Error loading SVG input.').setError(error)\n );\n }\n }\n\n // Export using options from the file\n if (exportOptions.infile && exportOptions.infile.length) {\n // Try to read the file to get the string representation\n try {\n log(4, '[chart] Attempting to export from an input file.');\n options.export.instr = readFileSync(exportOptions.infile, 'utf8');\n return exportAsString(options.export.instr.trim(), options, endCallback);\n } catch (error) {\n return endCallback(\n new ExportError('[chart] Error loading input file.').setError(error)\n );\n }\n }\n\n // Export with options from the raw representation\n if (\n (exportOptions.instr && exportOptions.instr !== '') ||\n (exportOptions.options && exportOptions.options !== '')\n ) {\n try {\n log(4, '[chart] Attempting to export from a raw input.');\n\n // Use whichever one is available\n exportOptions.instr = exportOptions.instr || exportOptions.options;\n\n // Perform a direct inject when forced\n if (toBoolean(options.customLogic?.allowCodeExecution)) {\n return doStraightInject(options, endCallback);\n }\n\n // Either try to parse to JSON first or do the direct export\n return typeof exportOptions.instr === 'string'\n ? exportAsString(exportOptions.instr.trim(), options, endCallback)\n : doExport(\n options,\n exportOptions.instr || exportOptions.options,\n endCallback\n );\n } catch (error) {\n return endCallback(\n new ExportError('[chart] Error loading raw input.').setError(error)\n );\n }\n }\n\n // No input specified, pass an error message to the callback\n return endCallback(\n new ExportError(\n `[chart] No valid input specified. Check if at least one of the following parameters is correctly set: 'infile', 'instr', 'options', or 'svg'.`\n )\n );\n};\n\n/**\n * Starts a batch export process for multiple charts based on the information\n * in the batch option. The batch is a string in the following format:\n * \"infile1.json=outfile1.png;infile2.json=outfile2.png;...\"\n *\n * @param {Object} options - The options object containing configuration for\n * a batch export.\n *\n * @returns {Promise} A Promise that resolves once the batch export\n * process is completed.\n *\n * @throws {ExportError} Throws an ExportError if an error occurs during\n * any of the batch export process.\n */\nexport const batchExport = async (options) => {\n const batchFunctions = [];\n\n // Split and pair the --batch arguments\n for (let pair of options.export.batch.split(';')) {\n pair = pair.split('=');\n if (pair.length === 2) {\n batchFunctions.push(\n startExport(\n {\n ...options,\n export: {\n ...options.export,\n infile: pair[0],\n outfile: pair[1]\n }\n },\n (error, info) => {\n // Throw an error\n if (error) {\n throw error;\n }\n\n // Save the base64 from a buffer to a correct image file\n writeFileSync(\n info.options.export.outfile,\n info.options.export.type !== 'svg'\n ? Buffer.from(info.result, 'base64')\n : info.result\n );\n }\n )\n );\n }\n }\n\n try {\n // Await all exports are done\n await Promise.all(batchFunctions);\n\n // Kill pool and close browser after finishing batch export\n await killPool();\n } catch (error) {\n throw new ExportError(\n '[chart] Error encountered during batch export.'\n ).setError(error);\n }\n};\n\n/**\n * Starts a single export process based on the specified options.\n *\n * @param {Object} options - The options object containing configuration for\n * a single export.\n *\n * @returns {Promise} A Promise that resolves once the single export\n * process is completed.\n *\n * @throws {ExportError} Throws an ExportError if an error occurs during\n * the single export process.\n */\nexport const singleExport = async (options) => {\n // Use instr or its alias, options\n options.export.instr = options.export.instr || options.export.options;\n\n // Perform an export\n await startExport(options, async (error, info) => {\n // Exit process when error\n if (error) {\n throw error;\n }\n\n const { outfile, type } = info.options.export;\n\n // Save the base64 from a buffer to a correct image file\n writeFileSync(\n outfile || `chart.${type}`,\n type !== 'svg' ? Buffer.from(info.result, 'base64') : info.result\n );\n\n // Kill pool and close browser after finishing single export\n await killPool();\n });\n};\n\n/**\n * Determines the size and scale for chart export based on the provided options.\n *\n * @param {Object} options - The options object containing configuration for\n * chart export.\n *\n * @returns {Object} An object containing the calculated height, width,\n * and scale for the chart export.\n */\nexport const findChartSize = (options) => {\n const { chart, exporting } =\n options.export?.options || isCorrectJSON(options.export?.instr);\n\n // See if globalOptions holds chart or exporting size\n const globalOptions = isCorrectJSON(options.export?.globalOptions);\n\n // Secure scale value\n let scale =\n options.export?.scale ||\n exporting?.scale ||\n globalOptions?.exporting?.scale ||\n options.export?.defaultScale ||\n 1;\n\n // the scale cannot be lower than 0.1 and cannot be higher than 5.0\n scale = Math.max(0.1, Math.min(scale, 5.0));\n\n // we want to round the numbers like 0.23234 -> 0.23\n scale = roundNumber(scale, 2);\n\n // Find chart size and scale\n const size = {\n height:\n options.export?.height ||\n exporting?.sourceHeight ||\n chart?.height ||\n globalOptions?.exporting?.sourceHeight ||\n globalOptions?.chart?.height ||\n options.export?.defaultHeight ||\n 400,\n width:\n options.export?.width ||\n exporting?.sourceWidth ||\n chart?.width ||\n globalOptions?.exporting?.sourceWidth ||\n globalOptions?.chart?.width ||\n options.export?.defaultWidth ||\n 600,\n scale\n };\n\n // Get rid of potential px and %\n for (let [param, value] of Object.entries(size)) {\n size[param] =\n typeof value === 'string' ? +value.replace(/px|%/gi, '') : value;\n }\n return size;\n};\n\n/**\n * Function for finalizing options before export.\n *\n * @param {Object} options - The options object containing configuration for\n * the export process.\n * @param {Object} chartJson - The JSON representation of the chart.\n * @param {Function} endCallback - The callback function to be called upon\n * completion or error.\n * @param {string} svg - The SVG representation of the chart.\n *\n * @returns {Promise} A Promise that resolves once the export process\n * is completed.\n */\nconst doExport = async (options, chartJson, endCallback, svg) => {\n let { export: exportOptions, customLogic: customLogicOptions } = options;\n\n const allowCodeExecutionScoped =\n typeof customLogicOptions.allowCodeExecution === 'boolean'\n ? customLogicOptions.allowCodeExecution\n : allowCodeExecution;\n\n if (!customLogicOptions) {\n customLogicOptions = options.customLogic = {};\n } else if (allowCodeExecutionScoped) {\n if (typeof options.customLogic.resources === 'string') {\n // Process resources\n options.customLogic.resources = handleResources(\n options.customLogic.resources,\n toBoolean(options.customLogic.allowFileResources)\n );\n } else if (!options.customLogic.resources) {\n try {\n const resources = readFileSync('resources.json', 'utf8');\n options.customLogic.resources = handleResources(\n resources,\n toBoolean(options.customLogic.allowFileResources)\n );\n } catch (error) {\n log(2, `[chart] Unable to load the default resources.json file.`);\n }\n }\n }\n\n // If the allowCodeExecution flag isn't set, we should refuse the usage\n // of callback, resources, and custom code. Additionally, the worker will\n // refuse to run arbitrary JavaScript. Prioritized should be the scoped\n // option, then we should take a look at the overall pool option.\n if (!allowCodeExecutionScoped && customLogicOptions) {\n if (\n customLogicOptions.callback ||\n customLogicOptions.resources ||\n customLogicOptions.customCode\n ) {\n // Send back a friendly message saying that the exporter does not support\n // these settings.\n return endCallback(\n new ExportError(\n `[chart] The 'callback', 'resources' and 'customCode' options have been disabled for this server.`\n )\n );\n }\n\n // Reset all additional custom code\n customLogicOptions.callback = false;\n customLogicOptions.resources = false;\n customLogicOptions.customCode = false;\n }\n\n // Clean properties to keep it lean and mean\n if (chartJson) {\n chartJson.chart = chartJson.chart || {};\n chartJson.exporting = chartJson.exporting || {};\n chartJson.exporting.enabled = false;\n }\n\n exportOptions.constr = exportOptions.constr || 'chart';\n exportOptions.type = fixType(exportOptions.type, exportOptions.outfile);\n if (exportOptions.type === 'svg') {\n exportOptions.width = false;\n }\n\n // Prepare global and theme options\n ['globalOptions', 'themeOptions'].forEach((optionsName) => {\n try {\n if (exportOptions && exportOptions[optionsName]) {\n if (\n typeof exportOptions[optionsName] === 'string' &&\n exportOptions[optionsName].endsWith('.json')\n ) {\n exportOptions[optionsName] = isCorrectJSON(\n readFileSync(exportOptions[optionsName], 'utf8'),\n true\n );\n } else {\n exportOptions[optionsName] = isCorrectJSON(\n exportOptions[optionsName],\n true\n );\n }\n }\n } catch (error) {\n exportOptions[optionsName] = {};\n logWithStack(2, error, `[chart] The '${optionsName}' cannot be loaded.`);\n }\n });\n\n // Prepare the customCode\n if (customLogicOptions.allowCodeExecution) {\n try {\n customLogicOptions.customCode = wrapAround(\n customLogicOptions.customCode,\n customLogicOptions.allowFileResources\n );\n } catch (error) {\n logWithStack(2, error, `[chart] The 'customCode' cannot be loaded.`);\n }\n }\n\n // Get the callback\n if (\n customLogicOptions &&\n customLogicOptions.callback &&\n customLogicOptions.callback?.indexOf('{') < 0\n ) {\n // The allowFileResources is always set to false for HTTP requests to avoid\n // injecting arbitrary files from the fs\n if (customLogicOptions.allowFileResources) {\n try {\n customLogicOptions.callback = readFileSync(\n customLogicOptions.callback,\n 'utf8'\n );\n } catch (error) {\n customLogicOptions.callback = false;\n logWithStack(2, error, `[chart] The 'callback' cannot be loaded.`);\n }\n } else {\n customLogicOptions.callback = false;\n }\n }\n\n // Size search\n options.export = {\n ...options.export,\n ...findChartSize(options)\n };\n\n // Post the work to the pool\n try {\n const result = await postWork(\n exportOptions.strInj || chartJson || svg,\n options\n );\n return endCallback(false, result);\n } catch (error) {\n return endCallback(error);\n }\n};\n\n/**\n * Performs a direct inject of options before export. The function attempts\n * to stringify the provided options and removes unnecessary characters,\n * ensuring a clean and formatted input. The resulting string is saved as\n * a \"stright inject\" string in the export options. It then invokes the\n * doExport function with the updated options.\n *\n * IMPORTANT: Dangerous and must be used deliberately by someone who sets up\n * a server (see the --allowCodeExecution option).\n *\n * @param {Object} options - The export options containing the input\n * to be injected.\n * @param {function} endCallback - The callback function to be invoked\n * at the end of the process.\n *\n * @returns {Promise} A Promise that resolves with the result of the export\n * operation or rejects with an error if any issues occur during the process.\n */\nconst doStraightInject = (options, endCallback) => {\n try {\n let strInj;\n let instr = options.export.instr || options.export.options;\n\n if (typeof instr !== 'string') {\n // Try to stringify options\n strInj = instr = optionsStringify(\n instr,\n options.customLogic?.allowCodeExecution\n );\n }\n strInj = instr.replaceAll(/\\t|\\n|\\r/g, '').trim();\n\n // Get rid of the ;\n if (strInj[strInj.length - 1] === ';') {\n strInj = strInj.substring(0, strInj.length - 1);\n }\n\n // Save as stright inject string\n options.export.strInj = strInj;\n return doExport(options, false, endCallback);\n } catch (error) {\n return endCallback(\n new ExportError(\n `[chart] Malformed input detected for ${options.export?.requestId || '?'}. Please make sure that your JSON/JavaScript options are sent using the \"options\" attribute, and that if you're using SVG, it is unescaped.`\n ).setError(error)\n );\n }\n};\n\n/**\n * Exports a string based on the provided options and invokes an end callback.\n *\n * @param {string} stringToExport - The string content to be exported.\n * @param {Object} options - Export options, including customLogic with\n * allowCodeExecution flag.\n * @param {Function} endCallback - Callback function to be invoked at the end\n * of the export process.\n *\n * @returns {any} Result of the export process or an error if encountered.\n */\nconst exportAsString = (stringToExport, options, endCallback) => {\n const { allowCodeExecution } = options.customLogic;\n\n // Check if it is SVG\n if (\n stringToExport.indexOf('= 0 ||\n stringToExport.indexOf('= 0\n ) {\n log(4, '[chart] Parsing input as SVG.');\n return doExport(options, false, endCallback, stringToExport);\n }\n\n try {\n // Try to parse to JSON and call the doExport function\n const chartJSON = JSON.parse(stringToExport.replaceAll(/\\t|\\n|\\r/g, ' '));\n\n // If a correct JSON, do the export\n return doExport(options, chartJSON, endCallback);\n } catch (error) {\n // Not a valid JSON\n if (toBoolean(allowCodeExecution)) {\n return doStraightInject(options, endCallback);\n } else {\n // Do not allow straight injection without the allowCodeExecution flag\n return endCallback(\n new ExportError(\n '[chart] Only JSON configurations and SVG are allowed for this server. If this is your server, JavaScript custom code can be enabled by starting the server with the --allowCodeExecution flag.'\n ).setError(error)\n );\n }\n }\n};\n\n/**\n * Retrieves and returns the current status of code execution permission.\n *\n * @returns {any} The value of allowCodeExecution.\n */\nexport const getAllowCodeExecution = () => allowCodeExecution;\n\n/**\n * Sets the code execution permission based on the provided boolean value.\n *\n * @param {any} value - The value to be converted and assigned\n * to allowCodeExecution.\n */\nexport const setAllowCodeExecution = (value) => {\n allowCodeExecution = toBoolean(value);\n};\n\nexport default {\n batchExport,\n singleExport,\n getAllowCodeExecution,\n setAllowCodeExecution,\n startExport,\n findChartSize\n};\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { log } from './logger.js';\n\n// Array that contains ids of all ongoing intervals\nconst intervalIds = [];\n\n/**\n * Adds id of a setInterval to the intervalIds array.\n *\n * @param {NodeJS.Timeout} id - Id of an interval.\n */\nexport const addInterval = (id) => {\n intervalIds.push(id);\n};\n\n/**\n * Clears all of ongoing intervals by ids gathered in the intervalIds array.\n */\nexport const clearAllIntervals = () => {\n log(4, `[server] Clearing all registered intervals.`);\n for (const id of intervalIds) {\n clearInterval(id);\n }\n};\n\nexport default {\n addInterval,\n clearAllIntervals\n};\n","import { envs } from '../envs.js';\nimport { logWithStack } from '../logger.js';\n\n/**\n * Middleware for logging errors with stack trace and handling error response.\n *\n * @param {Error} error - The error object.\n * @param {Express.Request} req - The Express request object.\n * @param {Express.Response} res - The Express response object.\n * @param {Function} next - The next middleware function.\n */\nconst logErrorMiddleware = (error, req, res, next) => {\n // Display the error with stack in a correct format\n logWithStack(1, error);\n\n // Delete the stack for the environment other than the development\n if (envs.OTHER_NODE_ENV !== 'development') {\n delete error.stack;\n }\n\n // Call the returnErrorMiddleware\n next(error);\n};\n\n/**\n * Middleware for returning error response.\n *\n * @param {Error} error - The error object.\n * @param {Express.Request} req - The Express request object.\n * @param {Express.Response} res - The Express response object.\n * @param {Function} next - The next middleware function.\n */\nconst returnErrorMiddleware = (error, req, res, next) => {\n // NOTE: Once the response has started there is no status left to set, and\n // handing the error onwards would let Express's default handler take\n // over, which answers 500. Ending the response is the only action here\n // that cannot produce one.\n if (res.headersSent) {\n return res.end();\n }\n\n // Gather all requied information for the response\n const { statusCode: stCode, status, message, stack, errorCode } = error;\n let statusCode = stCode || status || 400;\n\n // NOTE: This server must never answer with a 5xx. Treat that as an absolute\n // rule, not a preference.\n //\n // Nothing in this codebase sets a 5xx deliberately, but the status is not\n // always ours: setError copies statusCode up from a wrapped error, and\n // wrapped errors include ones from outbound HTTP calls, which can carry\n // any status a remote gave us. Clamping at the one place every error\n // response passes through makes a 5xx structurally impossible instead of\n // something to be careful about.\n //\n // It is logged loudly because reaching here means an error carried a\n // status it should not have, which is worth knowing about even though the\n // response is safe.\n if (statusCode >= 500 || statusCode < 100) {\n logWithStack(\n 1,\n error,\n `[server] An error carried the out-of-contract status ${statusCode}, answering with 400 instead. This server must never return a 5xx.`\n );\n\n statusCode = 400;\n }\n\n // Set and return response\n //\n // NOTE: The errorCode is only included when the error carries one, so that\n // the response shape is unchanged for errors that do not. It exists\n // because the status code alone cannot distinguish a request the server\n // refused because it was busy from one it refused because it was\n // malformed - both are reported as 400.\n res.status(statusCode).json({\n statusCode,\n message,\n stack,\n ...(errorCode ? { errorCode } : {})\n });\n};\n\nexport default (app) => {\n // Add log error middleware\n app.use(logErrorMiddleware);\n\n // Add set status and return error middleware\n app.use(returnErrorMiddleware);\n};\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport rateLimit from 'express-rate-limit';\n\nimport { log } from '../logger.js';\n\n/**\n * Middleware for enabling rate limiting on the specified Express app.\n *\n * @param {Express} app - The Express app instance.\n * @param {Object} limitConfig - Configuration options for rate limiting.\n */\nexport default (app, limitConfig) => {\n const msg =\n 'Too many requests, you have been rate limited. Please try again later.';\n\n // Options for the rate limiter\n const rateOptions = {\n max: limitConfig.maxRequests || 30,\n window: limitConfig.window || 1,\n trustProxy: limitConfig.trustProxy || false,\n skipKey: limitConfig.skipKey || false,\n skipToken: limitConfig.skipToken || false\n };\n\n // Set if behind a proxy\n if (rateOptions.trustProxy) {\n app.enable('trust proxy');\n }\n\n // Create a limiter\n const limiter = rateLimit({\n windowMs: rateOptions.window * 60 * 1000,\n // The number of requests each IP may make per window. Named `max` before\n // express-rate-limit v7.\n limit: rateOptions.max,\n handler: (request, response) => {\n response.format({\n json: () => {\n response.status(429).send({ message: msg });\n },\n default: () => {\n response.status(429).send(msg);\n }\n });\n },\n skip: (request) => {\n // Allow bypassing the limiter if a valid key/token has been sent\n if (\n rateOptions.skipKey !== false &&\n rateOptions.skipToken !== false &&\n request.query.key === rateOptions.skipKey &&\n request.query.access_token === rateOptions.skipToken\n ) {\n log(4, '[rate limiting] Skipping rate limiter.');\n return true;\n }\n return false;\n }\n });\n\n // Use a limiter as a middleware\n app.use(limiter);\n\n log(\n 3,\n `[rate limiting] Enabled rate limiting with ${rateOptions.max} requests per ${rateOptions.window} minute for each IP, trusting proxy: ${rateOptions.trustProxy}.`\n );\n};\n","import ExportError from './ExportError.js';\n\nclass HttpError extends ExportError {\n constructor(message, status, errorCode = false) {\n super(message);\n this.status = this.statusCode = status;\n\n if (errorCode) {\n this.errorCode = errorCode;\n }\n }\n\n setStatus(status) {\n this.status = status;\n return this;\n }\n}\n\nexport default HttpError;\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { updateVersion, version } from '../../cache.js';\nimport { envs } from '../../envs.js';\n\nimport HttpError from '../../errors/HttpError.js';\n\n/**\n * Adds the POST /change_hc_version/:newVersion route that can be utilized to modify\n * the Highcharts version on the server.\n *\n * TODO: Add auth token and connect to API\n */\nexport default (app) =>\n !app\n ? false\n : app.post(\n '/version/change/:newVersion',\n async (request, response, next) => {\n try {\n const adminToken = envs.HIGHCHARTS_ADMIN_TOKEN;\n\n // Check the existence of the token\n if (!adminToken || !adminToken.length) {\n throw new HttpError(\n 'The server is not configured to perform run-time version changes: HIGHCHARTS_ADMIN_TOKEN is not set.',\n 401\n );\n }\n\n // Check if the hc-auth header contain a correct token\n const token = request.get('hc-auth');\n if (!token || token !== adminToken) {\n throw new HttpError(\n 'Invalid or missing token: Set the token in the hc-auth header.',\n 401\n );\n }\n\n // Compare versions\n const newVersion = request.params.newVersion;\n\n // Accept only version strings containing digits, letters, dots, hyphens\n if (newVersion && /^[a-zA-Z0-9.-]+$/.test(newVersion)) {\n try {\n await updateVersion(newVersion);\n } catch (error) {\n throw new HttpError(\n `Version change: ${error.message}`,\n error.statusCode\n ).setError(error);\n }\n\n // Success\n response.status(200).send({\n statusCode: 200,\n version: version(),\n message: `Successfully updated Highcharts to version: ${newVersion}.`\n });\n } else {\n // No version specified\n throw new HttpError('No new version supplied.', 400);\n }\n } catch (error) {\n next(error);\n }\n }\n );\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { v4 as uuid } from 'uuid';\n\nimport { getAllowCodeExecution, startExport } from '../../chart.js';\nimport { getOptions, mergeConfigOptions } from '../../config.js';\nimport { log } from '../../logger.js';\nimport {\n fixType,\n isCorrectJSON,\n isObjectEmpty,\n isPrivateRangeUrlFound,\n optionsStringify,\n measureTime\n} from '../../utils.js';\n\nimport HttpError from '../../errors/HttpError.js';\nimport { errorCodes } from '../../errors/codes.js';\n\n// Reversed MIME types\nconst reversedMime = {\n png: 'image/png',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n pdf: 'application/pdf',\n svg: 'image/svg+xml'\n};\n\n// The requests counter\nlet requestsCounter = 0;\n\n// The array of callbacks to call before a request\nconst beforeRequest = [];\n\n// The array of callbacks to call after a request\nconst afterRequest = [];\n\n/**\n * Invokes an array of callback functions with specified parameters, allowing\n * customization of request handling.\n *\n * @param {Function[]} callbacks - An array of callback functions\n * to be executed.\n * @param {Express.Request} request - The Express request object.\n * @param {Express.Response} response - The Express response object.\n * @param {Object} data - An object containing parameters like id, uniqueId,\n * type, and body.\n *\n * @returns {boolean} - Returns a boolean indicating the overall result\n * of the callback invocations.\n */\nconst doCallbacks = (callbacks, request, response, data) => {\n let result = true;\n const { id, uniqueId, type, body } = data;\n\n callbacks.some((callback) => {\n if (callback) {\n let callResponse = callback(request, response, id, uniqueId, type, body);\n\n if (callResponse !== undefined && callResponse !== true) {\n result = callResponse;\n }\n\n return true;\n }\n });\n\n return result;\n};\n\n/**\n * Handles the export requests from the client.\n *\n * @param {Express.Request} request - The Express request object.\n * @param {Express.Response} response - The Express response object.\n * @param {Function} next - The next middleware function.\n *\n * @returns {Promise} - A promise that resolves once the export process\n * is complete.\n */\nconst exportHandler = async (request, response, next) => {\n try {\n // Start counting time\n const stopCounter = measureTime();\n\n // Create a unique ID for a request\n const uniqueId = uuid().replace(/-/g, '');\n\n // Get the current server's general options\n const defaultOptions = getOptions();\n\n const body = request.body;\n const id = ++requestsCounter;\n\n let type = fixType(body.type);\n\n // Throw 'Bad Request' if there's no body\n if (!body || isObjectEmpty(body)) {\n throw new HttpError(\n 'The request body is required. Please ensure that your Content-Type header is correct (accepted types are application/json and multipart/form-data).',\n 400,\n errorCodes.INVALID_REQUEST\n );\n }\n\n // All of the below can be used\n let instr = isCorrectJSON(body.infile || body.options || body.data);\n\n // Throw 'Bad Request' if there's no JSON or SVG to export\n if (!instr && !body.svg) {\n log(\n 2,\n `The request with ID ${uniqueId} from ${\n request.headers['x-forwarded-for'] || request.connection.remoteAddress\n } was incorrect:\n Content-Type: ${request.headers['content-type']}. \n Chart constructor: ${body.constr}.\n Dimensions: ${body.width}x${body.height} @ ${body.scale} scale.\n Type: ${type}.\n Is SVG set? ${typeof body.svg !== 'undefined'}.\n B64? ${typeof body.b64 !== 'undefined'}.\n No download? ${typeof body.noDownload !== 'undefined'}.\n\n Payload received: ${JSON.stringify(body.infile || body.options || body.data || body.svg)}\n\n `\n );\n\n throw new HttpError(\n \"No correct chart data found. Ensure that you are using either application/json or multipart/form-data headers. If sending JSON, make sure the chart data is in the 'infile', 'options', or 'data' attribute. If sending SVG, ensure it is in the 'svg' attribute.\",\n 400,\n errorCodes.INVALID_REQUEST\n );\n }\n\n let callResponse = false;\n\n // Call the before request functions\n callResponse = doCallbacks(beforeRequest, request, response, {\n id,\n uniqueId,\n type,\n body\n });\n\n // Block the request if one of a callbacks failed\n if (callResponse !== true) {\n return response.send(callResponse);\n }\n\n // NOTE: Notice the client going away, so that work nobody is waiting for can\n // be dropped rather than occupying the queue and then a worker.\n //\n // This listens on the response rather than the socket, and does not\n // require the close to have carried an error. The previous check only\n // set the flag when the socket closed with `hadErrors`, so a clean\n // disconnect - a proxy idle timeout, or a caller cancelling - was not\n // detected at all. writableFinished distinguishes the client leaving\n // early from the normal close after a completed response.\n const abortController = new AbortController();\n\n response.once('close', () => {\n if (!response.writableFinished) {\n abortController.abort();\n\n log(\n 4,\n `[export] The client closed the connection for request ${uniqueId} before it was answered.`\n );\n }\n });\n\n log(4, `[export] Got an incoming HTTP request with ID ${uniqueId}.`);\n\n body.constr = (typeof body.constr === 'string' && body.constr) || 'chart';\n\n // Gather and organize options from the payload\n const requestOptions = {\n export: {\n instr,\n type,\n constr: body.constr[0].toLowerCase() + body.constr.substr(1),\n height: body.height,\n width: body.width,\n scale: body.scale || defaultOptions.export.scale,\n globalOptions: isCorrectJSON(body.globalOptions, true),\n themeOptions: isCorrectJSON(body.themeOptions, true)\n },\n customLogic: {\n allowCodeExecution: getAllowCodeExecution(),\n allowFileResources: false,\n resources: isCorrectJSON(body.resources, true),\n callback: body.callback,\n customCode: body.customCode\n }\n };\n\n if (instr) {\n // Stringify JSON with options\n requestOptions.export.instr = optionsStringify(\n instr,\n requestOptions.customLogic.allowCodeExecution\n );\n }\n\n // Merge the request options into default ones\n const options = mergeConfigOptions(defaultOptions, requestOptions);\n\n // Save the JSON if exists\n options.export.options = instr;\n\n // Lastly, add the server specific arguments into options as payload\n options.payload = {\n svg: body.svg || false,\n b64: body.b64 || false,\n noDownload: body.noDownload || false,\n requestId: uniqueId,\n // Lets the pool drop this export if the client gives up before a worker\n // becomes free\n abortSignal: abortController.signal\n };\n\n // Test xlink:href elements from payload's SVG\n if (body.svg && isPrivateRangeUrlFound(options.payload.svg)) {\n throw new HttpError(\n 'SVG potentially contain at least one forbidden URL in xlink:href element. Please review the SVG content and ensure that all referenced URLs comply with security policies.',\n 400,\n errorCodes.INVALID_REQUEST\n );\n }\n\n // Start the export process\n await startExport(options, (error, info) => {\n // NOTE: Check this before looking at the error. An abandoned export is\n // reported as an error by design, and there is nobody left to answer,\n // so raising it here would only produce a failed write to a closed\n // socket and a misleading error in the log.\n if (abortController.signal.aborted) {\n return log(\n 4,\n `[export] Discarding the result for request ${uniqueId}: the client is gone.`\n );\n }\n\n // After the whole exporting process\n if (defaultOptions.server.benchmarking) {\n log(\n 5,\n `[benchmark] Request with ID ${uniqueId} - After the whole exporting process: ${stopCounter()}ms.`\n );\n }\n\n // If error, log it and send it to the error middleware\n if (error) {\n throw error;\n }\n\n // If data is missing, log the message and send it to the error middleware\n if (!info || !info.result) {\n throw new HttpError(\n `Unexpected return from chart generation. Please check your request data. For the request with ID ${uniqueId}, the result is ${info.result}.`,\n 400,\n errorCodes.EXPORT_FAILED\n );\n }\n\n // Get the type from options\n type = info.options.export.type;\n\n // The after request callbacks\n doCallbacks(afterRequest, request, response, { id, body: info.result });\n\n if (info.result) {\n // If only base64 is required, return it\n if (body.b64) {\n // SVG Exception for the Highcharts 11.3.0 version\n if (type === 'pdf' || type == 'svg') {\n return response.send(\n Buffer.from(info.result, 'utf8').toString('base64')\n );\n }\n\n return response.send(info.result);\n }\n\n // Set correct content type\n response.header('Content-Type', reversedMime[type] || 'image/png');\n\n // Decide whether to download or not chart file\n if (!body.noDownload) {\n response.attachment(\n `${request.params.filename || request.body.filename || 'chart'}.${\n type || 'png'\n }`\n );\n }\n\n // If SVG, return plain content\n return type === 'svg'\n ? response.send(info.result)\n : response.send(Buffer.from(info.result, 'base64'));\n }\n });\n } catch (error) {\n next(error);\n }\n};\n\nexport default (app) => {\n /**\n * Adds the POST / a route for handling POST requests at the root endpoint.\n */\n app.post('/', exportHandler);\n\n /**\n * Adds the POST /:filename a route for handling POST requests with\n * a specified filename parameter.\n */\n app.post('/:filename', exportHandler);\n};\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { readFileSync } from 'fs';\nimport { join as pather } from 'path';\nimport { log } from '../../logger.js';\n\nimport { version } from '../../cache.js';\nimport { addInterval } from '../../intervals.js';\nimport pool, { getConsecutiveCreateFailures } from '../../pool.js';\nimport { isConnected as browserIsConnected } from '../../browser.js';\nimport { __dirname } from '../../utils.js';\n\nconst pkgFile = JSON.parse(readFileSync(pather(__dirname, 'package.json')));\n\nconst serverStartTime = new Date();\n\nconst successRates = [];\nconst recordInterval = 60 * 1000; // record every minute\nconst windowSize = 30; // 30 minutes\n\n/**\n * Calculates moving average indicator based on the data from the successRates\n * array.\n *\n * @returns {number} - A moving average for success ratio of the server exports.\n */\nfunction calculateMovingAverage() {\n const sum = successRates.reduce((a, b) => a + b, 0);\n return sum / successRates.length;\n}\n\n/**\n * Starts the interval responsible for calculating current success rate ratio\n * and gathers\n *\n * @returns {NodeJS.Timeout} id - Id of an interval.\n */\nexport const startSuccessRate = () =>\n setInterval(() => {\n const successRatio = calculateSuccessRatio(pool.getStats());\n\n successRates.push(successRatio === null ? 1 : successRatio);\n if (successRates.length > windowSize) {\n successRates.shift();\n }\n }, recordInterval);\n\n/**\n * Calculates the ratio of exports that succeeded, as a percentage.\n *\n * Exports abandoned by their client are excluded from the denominator. They are\n * not failures of the server - the caller went away - and counting them would\n * report a healthy server as failing whenever callers time out or cancel, which\n * is exactly when this figure is most likely to be looked at.\n *\n * @param {Object} stats - The pool statistics.\n *\n * @returns {number|null} The success ratio as a percentage, or null when no\n * export has been attempted yet.\n */\nfunction calculateSuccessRatio(stats) {\n const attempts = stats.exportAttempts - stats.abandonedExports;\n\n if (attempts <= 0) {\n return null;\n }\n\n return (stats.performedExports / attempts) * 100;\n}\n\n/**\n * Adds the /health and /success-moving-average routes\n * which output basic stats for the server.\n */\nexport default function addHealthRoutes(app) {\n if (!app) {\n return false;\n }\n\n // Start processing success rate ratio interval and save its id to the array\n // for the graceful clearing on shutdown with injected addInterval funtion\n addInterval(startSuccessRate());\n\n app.get('/health', (_, res) => {\n const stats = pool.getStats();\n const period = successRates.length;\n const movingAverage = calculateMovingAverage();\n\n log(4, '[health.js] GET /health [200] - returning server health.');\n\n res.send({\n status: 'OK',\n bootTime: serverStartTime,\n uptime:\n Math.floor(\n (new Date().getTime() - serverStartTime.getTime()) / 1000 / 60\n ) + ' minutes',\n version: pkgFile.version,\n highchartsVersion: version(),\n averageProcessingTime: stats.spentAverage,\n performedExports: stats.performedExports,\n failedExports: stats.droppedExports,\n abandonedExports: stats.abandonedExports,\n rejectedForCapacity: stats.rejectedForCapacity,\n consecutiveCreateFailures: getConsecutiveCreateFailures(),\n browserConnected: browserIsConnected(),\n exportAttempts: stats.exportAttempts,\n sucessRatio: calculateSuccessRatio(stats),\n // eslint-disable-next-line import/no-named-as-default-member\n pool: pool.getPoolInfoJSON(),\n\n // Moving average\n period,\n movingAverage,\n message:\n isNaN(movingAverage) || !successRates.length\n ? 'Too early to report. No exports made yet. Please check back soon.'\n : `Last ${period} minutes had a success rate of ${movingAverage.toFixed(2)}%.`,\n\n // SVG/JSON attempts\n svgExportAttempts: stats.exportFromSvgAttempts,\n jsonExportAttempts: stats.performedExports - stats.exportFromSvgAttempts\n });\n });\n}\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { promises as fsPromises } from 'fs';\nimport { posix } from 'path';\n\nimport cors from 'cors';\nimport express from 'express';\nimport http from 'http';\nimport https from 'https';\nimport multer from 'multer';\n\nimport errorHandler from './error.js';\nimport rateLimit from './rate_limit.js';\nimport { log, logWithStack } from '../logger.js';\nimport {\n getPool,\n getQueueLimit,\n getQueueRejectDelay,\n stats as poolStats\n} from '../pool.js';\nimport { __dirname } from '../utils.js';\n\nimport { errorCodes } from '../errors/codes.js';\nimport HttpError from '../errors/HttpError.js';\n\nimport vSwitchRoute from './routes/change_hc_version.js';\nimport exportRoutes from './routes/export.js';\nimport healthRoute from './routes/health.js';\nimport uiRoute from './routes/ui.js';\n\nimport ExportError from '../errors/ExportError.js';\n\n// Array of an active servers\nconst activeServers = new Map();\n\n// Create express app\nconst app = express();\n\n// Disable the X-Powered-By header\napp.disable('x-powered-by');\n\n// Enable CORS support\napp.use(cors());\n\n// Getting a lot of RangeNotSatisfiableError exception.\n// Even though this is a deprecated options, let's try to set it to false.\napp.use((_req, res, next) => {\n res.set('Accept-Ranges', 'none');\n next();\n});\n\n/**\n * Attach error handlers to the server.\n *\n * @param {http.Server} server - The HTTP/HTTPS server instance.\n */\nconst attachServerErrorHandlers = (server) => {\n server.on('clientError', (error, socket) => {\n logWithStack(\n 1,\n error,\n `[server] Client error: ${error.message}, destroying socket.`\n );\n socket.destroy();\n });\n\n server.on('error', (error) => {\n logWithStack(1, error, `[server] Server error: ${error.message}`);\n });\n\n server.on('connection', (socket) => {\n socket.on('error', (error) => {\n logWithStack(1, error, `[server] Socket error: ${error.message}`);\n });\n });\n};\n\n/**\n * Applies the connection timeouts to a server.\n *\n * NOTE: Node's default keepAliveTimeout is 5 seconds, which is shorter than the\n * idle timeout of a typical proxy or load balancer sitting in front of this\n * server - commonly 60. When the shorter side closes an idle connection the\n * other side does not know, so it can send a request into a connection that\n * is already going away, and the caller sees a gateway error that has\n * nothing to do with the request. Keeping ours longer than theirs leaves the\n * closing to them.\n *\n * headersTimeout is kept above keepAliveTimeout deliberately: if it were\n * shorter it would fire while a kept-alive connection was still legitimately\n * idle between requests.\n *\n * @param {http.Server} server - The HTTP/HTTPS server instance.\n * @param {Object} serverConfig - The server configuration object.\n */\nconst applyServerTimeouts = (server, serverConfig) => {\n const keepAlive = parseInt(serverConfig.keepAliveTimeout);\n const keepAliveTimeout =\n isNaN(keepAlive) || keepAlive < 0 ? 65000 : keepAlive;\n\n server.keepAliveTimeout = keepAliveTimeout;\n server.headersTimeout = keepAliveTimeout + 5000;\n\n log(\n 4,\n `[server] Set keepAliveTimeout to ${server.keepAliveTimeout}ms and headersTimeout to ${server.headersTimeout}ms.`\n );\n};\n\n/**\n * Starts an HTTP server based on the provided configuration. The `serverConfig`\n * object contains all server related properties (see the `server` section\n * in the `lib/schemas/config.js` file for a reference).\n *\n * @param {Object} serverConfig - The server configuration object.\n *\n * @throws {ExportError} - Throws an error if the server cannot be configured\n * and started.\n */\nexport const startServer = async (serverConfig) => {\n try {\n // TODO: Read from config/env\n // NOTE:\n // Too big limits lead to timeouts in the export process when the\n // rasterization timeout is set too low.\n const uploadLimitMiB = serverConfig.maxUploadSize || 3;\n const uploadLimitBytes = uploadLimitMiB * 1024 * 1024;\n\n // Enable parsing of form data (files) with Multer package\n const storage = multer.memoryStorage();\n const upload = multer({\n storage,\n limits: {\n fieldSize: uploadLimitBytes\n }\n });\n\n // NOTE: Refuse work before the body is parsed when the queue is already\n // full. Checking only inside the pool would mean a request that is\n // going to be refused anyway has its body - up to maxUploadSize, 3MiB\n // by default - read and held in memory first. With a deep queue that\n // is exactly the memory pressure a saturated server cannot afford,\n // and it is what eventually gets the browser killed.\n app.use((request, response, next) => {\n // Only export requests are worth gating; the admin version route is not\n // pool work and must stay reachable when the server is busy\n if (request.method !== 'POST' || request.path.startsWith('/version/')) {\n return next();\n }\n\n const pool = getPool();\n\n if (pool && pool.numPendingAcquires() >= getQueueLimit()) {\n ++poolStats.rejectedForCapacity;\n\n const error = new HttpError(\n `The server is at capacity: ${pool.numPendingAcquires()} exports are already waiting for a worker (limit is ${getQueueLimit()}). Please retry shortly.`,\n 400,\n errorCodes.QUEUE_FULL\n );\n\n // NOTE: Do not answer immediately by default. Measured: with an instant\n // refusal, clients that retry as soon as they are refused drive\n // the request rate up by orders of magnitude - 150 concurrent\n // clients reached 11000 requests per second - and the server then\n // spends its entire event loop refusing them, starving the exports\n // already in progress. Goodput fell from ~19 exports per second to\n // under 1.\n //\n // The 5 second acquire timeout used to provide this backpressure\n // accidentally, by making every client wait before it could retry.\n // Bounding the queue removes that, so the delay puts it back\n // deliberately, and far more cheaply: no body has been parsed and\n // no worker is held, only a socket and a timer.\n const delay = getQueueRejectDelay();\n\n if (!delay) {\n return next(error);\n }\n\n const onClose = () => clearTimeout(timer);\n\n const timer = setTimeout(() => {\n response.removeListener('close', onClose);\n next(error);\n }, delay);\n\n // Do not keep a timer alive for a client that has already gone\n response.once('close', onClose);\n\n return;\n }\n\n next();\n });\n\n // Enable body parser\n app.use(express.json({ limit: uploadLimitBytes }));\n app.use(express.urlencoded({ extended: true, limit: uploadLimitBytes }));\n\n // Use only non-file multipart form fields\n app.use(upload.none());\n\n // Stop if not enabled\n if (!serverConfig.enable) {\n return false;\n }\n\n // Listen HTTP server\n if (!serverConfig.ssl.force) {\n // Main server instance (HTTP)\n const httpServer = http.createServer(app);\n\n // Attach error handlers and listen to the server\n attachServerErrorHandlers(httpServer);\n applyServerTimeouts(httpServer, serverConfig);\n\n // Listen\n httpServer.listen(serverConfig.port, serverConfig.host);\n\n // Save the reference to HTTP server\n activeServers.set(serverConfig.port, httpServer);\n\n log(\n 3,\n `[server] Started HTTP server on ${serverConfig.host}:${serverConfig.port}.`\n );\n }\n\n // Listen HTTPS server\n if (serverConfig.ssl.enable) {\n // Set up an SSL server also\n let key, cert;\n\n try {\n // Get the SSL key\n key = await fsPromises.readFile(\n posix.join(serverConfig.ssl.certPath, 'server.key'),\n 'utf8'\n );\n\n // Get the SSL certificate\n cert = await fsPromises.readFile(\n posix.join(serverConfig.ssl.certPath, 'server.crt'),\n 'utf8'\n );\n } catch (error) {\n log(\n 2,\n `[server] Unable to load key/certificate from the '${serverConfig.ssl.certPath}' path. Could not run secured layer server.`\n );\n }\n\n if (key && cert) {\n // Main server instance (HTTPS)\n const httpsServer = https.createServer({ key, cert }, app);\n\n // Attach error handlers and listen to the server\n attachServerErrorHandlers(httpsServer);\n applyServerTimeouts(httpsServer, serverConfig);\n\n // Listen\n httpsServer.listen(serverConfig.ssl.port, serverConfig.host);\n\n // Save the reference to HTTPS server\n activeServers.set(serverConfig.ssl.port, httpsServer);\n\n log(\n 3,\n `[server] Started HTTPS server on ${serverConfig.host}:${serverConfig.ssl.port}.`\n );\n }\n }\n\n // Enable the rate limiter if config says so\n if (\n serverConfig.rateLimiting &&\n serverConfig.rateLimiting.enable &&\n ![0, NaN].includes(serverConfig.rateLimiting.maxRequests)\n ) {\n rateLimit(app, serverConfig.rateLimiting);\n }\n\n // Set up static folder's route\n app.use(express.static(posix.join(__dirname, 'public')));\n\n // Set up routes\n healthRoute(app);\n exportRoutes(app);\n uiRoute(app);\n vSwitchRoute(app);\n\n // Set up centralized error handler\n errorHandler(app);\n } catch (error) {\n throw new ExportError(\n '[server] Could not configure and start the server.'\n ).setError(error);\n }\n};\n\n/**\n * Closes all servers associated with Express app instance, resolving once the\n * requests already in flight have been served.\n *\n * @returns {Promise} Resolves when every server has closed.\n */\nexport const closeServers = () => {\n log(4, `[server] Closing all servers.`);\n\n return Promise.all(\n [...activeServers].map(\n ([port, server]) =>\n new Promise((resolve) => {\n // close() stops new connections being accepted and calls back once the\n // ones in flight have finished, which is what makes a graceful shutdown\n // possible - the previous version discarded that callback entirely\n server.close(() => {\n activeServers.delete(port);\n log(4, `[server] Closed server on port: ${port}.`);\n resolve();\n });\n\n // NOTE: Without this, close() also waits for connections that are merely\n // idle, which now means up to keepAliveTimeout - 65 seconds. Idle\n // connections have no work worth waiting for, so end them and let\n // close() wait only on requests actually being served.\n server.closeIdleConnections?.();\n })\n )\n );\n};\n\n/**\n * Get all servers associated with Express app instance.\n *\n * @returns {Array} - Servers associated with Express app instance.\n */\nexport const getServers = () => activeServers;\n\n/**\n * Enable rate limiting for the server.\n *\n * @param {Object} limitConfig - Configuration object for rate limiting.\n */\nexport const enableRateLimiting = (limitConfig) => rateLimit(app, limitConfig);\n\n/**\n * Get the Express instance.\n *\n * @returns {Object} - The Express instance.\n */\nexport const getExpress = () => express;\n\n/**\n * Get the Express app instance.\n *\n * @returns {Object} - The Express app instance.\n */\nexport const getApp = () => app;\n\n/**\n * Apply middleware(s) to a specific path.\n *\n * @param {string} path - The path to which the middleware(s) should be applied.\n * @param {...Function} middlewares - The middleware functions to be applied.\n */\nexport const use = (path, ...middlewares) => {\n app.use(path, ...middlewares);\n};\n\n/**\n * Set up a route with GET method and apply middleware(s).\n *\n * @param {string} path - The route path.\n * @param {...Function} middlewares - The middleware functions to be applied.\n */\nexport const get = (path, ...middlewares) => {\n app.get(path, ...middlewares);\n};\n\n/**\n * Set up a route with POST method and apply middleware(s).\n *\n * @param {string} path - The route path.\n * @param {...Function} middlewares - The middleware functions to be applied.\n */\nexport const post = (path, ...middlewares) => {\n app.post(path, ...middlewares);\n};\n\nexport default {\n startServer,\n closeServers,\n getServers,\n enableRateLimiting,\n getExpress,\n getApp,\n use,\n get,\n post\n};\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { join } from 'path';\n\nimport { __dirname } from '../../utils.js';\n\n/**\n * Adds the GET / route for a UI when enabled on the export server.\n */\nexport default (app) =>\n !app\n ? false\n : app.get('/', (_request, response) => {\n response.sendFile(join(__dirname, 'public', 'index.html'), {\n acceptRanges: false\n });\n });\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport { getOptions } from './config.js';\nimport { clearAllIntervals } from './intervals.js';\nimport { killPool } from './pool.js';\nimport { closeServers } from './server/server.js';\n\n/**\n * Returns how long to let in-flight requests finish during a shutdown.\n *\n * @returns {number} The drain timeout in milliseconds.\n */\nconst getDrainTimeout = () => {\n const configured = parseInt(getOptions()?.other?.shutdownDrainTimeout);\n return isNaN(configured) || configured < 0 ? 30000 : configured;\n};\n\n/**\n * Clean up function to trigger before ending process for the graceful shutdown.\n *\n * @param {number} exitCode - An exit code for the process.exit() function.\n */\nexport const shutdownCleanUp = async (exitCode) => {\n // Stop the background intervals; nothing depends on their timing from here\n clearAllIntervals();\n\n // NOTE: These have to happen in order, not together. Previously all three were\n // started at once and closeServers() was not even awaitable - it is not\n // async and returned undefined, so Promise.allSettled treated it as\n // already done. The process therefore exited as soon as the pool had been\n // destroyed, cutting off every export still being served. That happens on\n // each restart, deploy and scale-in, so the dropped requests were not rare.\n //\n // Closing the servers first stops new work arriving and resolves once the\n // requests in flight have been answered, which is the drain. It is raced\n // against a timeout so that one request which never completes cannot hold\n // the shutdown open indefinitely.\n const drainTimeout = getDrainTimeout();\n\n await Promise.race([\n closeServers(),\n new Promise((resolve) => setTimeout(resolve, drainTimeout))\n ]);\n\n // Only once nothing is being served is it safe to take the workers away\n await killPool();\n\n // Exit process with a correct code\n process.exit(exitCode);\n};\n\nexport default {\n shutdownCleanUp\n};\n","/*******************************************************************************\n\nHighcharts Export Server\n\nCopyright (c) 2016-2024, Highsoft\n\nLicenced under the MIT licence.\n\nAdditionally a valid Highcharts license is required for use.\n\nSee LICENSE file in root for details.\n\n*******************************************************************************/\n\nimport 'colors';\n\nimport { checkAndUpdateCache } from './cache.js';\nimport {\n batchExport,\n setAllowCodeExecution,\n singleExport,\n startExport\n} from './chart.js';\nimport { mapToNewConfig, manualConfig, setOptions } from './config.js';\nimport {\n initLogging,\n log,\n logWithStack,\n setLogLevel,\n enableFileLogging\n} from './logger.js';\nimport { initPool, killPool } from './pool.js';\nimport { shutdownCleanUp } from './resource_release.js';\nimport server, { startServer } from './server/server.js';\nimport { printLogo, printUsage } from './utils.js';\n\n/**\n * Attaches exit listeners to the process, ensuring proper cleanup of resources\n * and termination on exit signals. Handles 'exit', 'SIGINT', 'SIGTERM', and\n * 'uncaughtException' events.\n */\nconst attachProcessExitListeners = () => {\n log(3, '[process] Attaching exit listeners to the process.');\n\n // Handler for the 'exit'\n process.on('exit', (code) => {\n log(4, `Process exited with code ${code}.`);\n });\n\n // Handler for the 'SIGINT'\n process.on('SIGINT', async (name, code) => {\n log(4, `The ${name} event with code: ${code}.`);\n await shutdownCleanUp(0);\n });\n\n // Handler for the 'SIGTERM'\n process.on('SIGTERM', async (name, code) => {\n log(4, `The ${name} event with code: ${code}.`);\n await shutdownCleanUp(0);\n });\n\n // Handler for the 'SIGHUP'\n process.on('SIGHUP', async (name, code) => {\n log(4, `The ${name} event with code: ${code}.`);\n await shutdownCleanUp(0);\n });\n\n // Handler for the 'uncaughtException'\n process.on('uncaughtException', async (error, name) => {\n logWithStack(1, error, `The ${name} error.`);\n await shutdownCleanUp(1);\n });\n};\n\n/**\n * Initializes the export process. Tasks such as configuring logging, checking\n * cache and sources, and initializing the pool of resources happen during\n * this stage. Function that is required to be called before trying to export charts or setting a server. The `options` is an object that contains all options.\n *\n * @param {Object} options - All export options.\n *\n * @returns {Promise} Promise resolving to the updated export options.\n */\nconst initExport = async (options) => {\n // Set the allowCodeExecution per export module scope\n setAllowCodeExecution(\n options.customLogic && options.customLogic.allowCodeExecution\n );\n\n // Init the logging\n initLogging(options.logging);\n\n // Attach process' exit listeners\n if (options.other.listenToProcessExits) {\n attachProcessExitListeners();\n }\n\n // Check if cache needs to be updated\n await checkAndUpdateCache(options);\n\n // Init the pool\n await initPool({\n pool: options.pool || {\n minWorkers: 1,\n maxWorkers: 1\n },\n puppeteerArgs: options.puppeteer.args || []\n });\n\n // Return updated options\n return options;\n};\n\nexport default {\n // Server\n server,\n startServer,\n\n // Exporting\n initExport,\n singleExport,\n batchExport,\n startExport,\n\n // Pool\n initPool,\n killPool,\n\n // Other\n setOptions,\n shutdownCleanUp,\n\n // Logs\n log,\n logWithStack,\n setLogLevel,\n enableFileLogging,\n\n // Utils\n mapToNewConfig,\n manualConfig,\n printLogo,\n printUsage\n};\n"],"names":["scriptsNames","core","modules","indicators","custom","defaultConfig","puppeteer","args","value","type","description","launchRetryWindow","envLink","tempDir","highcharts","version","cdnURL","useNpm","coreScripts","moduleScripts","indicatorScripts","customScripts","forceFetch","cachePath","export","infile","instr","options","outfile","constr","defaultHeight","defaultWidth","defaultScale","height","width","scale","globalOptions","themeOptions","batch","rasterizationTimeout","customLogic","allowCodeExecution","allowFileResources","customCode","callback","resources","loadConfig","legacyName","createConfig","server","maxUploadSize","enable","cliName","host","port","keepAliveTimeout","benchmarking","proxy","username","password","timeout","rateLimiting","maxRequests","window","trustProxy","skipKey","skipToken","ssl","force","certPath","pool","minWorkers","maxWorkers","workLimit","queueLimit","queueRejectDelay","acquireTimeout","createTimeout","destroyTimeout","idleTimeout","createRetryInterval","reaperInterval","logging","level","file","dest","toConsole","toFile","ui","route","other","nodeEnv","listenToProcessExits","noLogo","hardResetPage","shutdownDrainTimeout","browserShellMode","debug","headless","devtools","listenToConsole","dumpio","slowMo","debuggingPort","promptsConfig","name","message","initial","join","separator","instructions","choices","hint","min","max","round","absoluteProps","nestedArgs","createNestedArgs","obj","propChain","Object","keys","forEach","k","includes","entry","substring","undefined","dotenv","config","quiet","v","filterArray","z","string","transform","split","map","trim","filter","length","enum","values","refine","error","issue","input","test","isNaN","parseFloat","envs","object","PUPPETEER_TEMP_DIR","PUPPETEER_LAUNCH_RETRY_WINDOW","HIGHCHARTS_VERSION","HIGHCHARTS_CDN_URL","startsWith","HIGHCHARTS_USE_NPM","HIGHCHARTS_CORE_SCRIPTS","HIGHCHARTS_MODULE_SCRIPTS","HIGHCHARTS_INDICATOR_SCRIPTS","HIGHCHARTS_FORCE_FETCH","HIGHCHARTS_CACHE_PATH","HIGHCHARTS_ADMIN_TOKEN","EXPORT_TYPE","EXPORT_CONSTR","EXPORT_DEFAULT_HEIGHT","EXPORT_DEFAULT_WIDTH","EXPORT_DEFAULT_SCALE","EXPORT_RASTERIZATION_TIMEOUT","CUSTOM_LOGIC_ALLOW_CODE_EXECUTION","CUSTOM_LOGIC_ALLOW_FILE_RESOURCES","SERVER_ENABLE","SERVER_HOST","SERVER_PORT","SERVER_MAX_UPLOAD_SIZE","SERVER_KEEP_ALIVE_TIMEOUT","SERVER_BENCHMARKING","SERVER_PROXY_HOST","SERVER_PROXY_PORT","SERVER_PROXY_USERNAME","SERVER_PROXY_PASSWORD","SERVER_PROXY_TIMEOUT","SERVER_RATE_LIMITING_ENABLE","SERVER_RATE_LIMITING_MAX_REQUESTS","SERVER_RATE_LIMITING_WINDOW","SERVER_RATE_LIMITING_TRUST_PROXY","SERVER_RATE_LIMITING_SKIP_KEY","SERVER_RATE_LIMITING_SKIP_TOKEN","SERVER_SSL_ENABLE","SERVER_SSL_FORCE","SERVER_SSL_PORT","SERVER_SSL_CERT_PATH","POOL_MIN_WORKERS","POOL_MAX_WORKERS","POOL_WORK_LIMIT","POOL_QUEUE_LIMIT","POOL_QUEUE_REJECT_DELAY","POOL_ACQUIRE_TIMEOUT","POOL_CREATE_TIMEOUT","POOL_DESTROY_TIMEOUT","POOL_IDLE_TIMEOUT","POOL_CREATE_RETRY_INTERVAL","POOL_REAPER_INTERVAL","POOL_BENCHMARKING","LOGGING_LEVEL","LOGGING_FILE","LOGGING_DEST","LOGGING_TO_CONSOLE","LOGGING_TO_FILE","UI_ENABLE","UI_ROUTE","OTHER_SHUTDOWN_DRAIN_TIMEOUT","OTHER_NODE_ENV","OTHER_LISTEN_TO_PROCESS_EXITS","OTHER_NO_LOGO","OTHER_HARD_RESET_PAGE","OTHER_BROWSER_SHELL_MODE","OTHER_ALLOW_XLINK","DEBUG_ENABLE","DEBUG_HEADLESS","DEBUG_DEVTOOLS","DEBUG_LISTEN_TO_CONSOLE","DEBUG_DUMPIO","DEBUG_SLOW_MO","DEBUG_DEBUGGING_PORT","partial","parse","process","env","colors","pathCreated","levelsDesc","title","color","listeners","logToFile","texts","prefix","existsSync","mkdirSync","appendFile","concat","console","log","newLevel","Date","toString","fn","apply","logWithStack","customMessage","mainMessage","stackMessage","stack","slice","setLogLevel","enableFileLogging","logDest","logFile","endsWith","__highchartsDir","dirname","createRequire","url","resolve","__dirname","fileURLToPath","URL","fixType","formats","outType","pop","find","t","handleResources","allowedProps","handledResources","correctResources","isCorrectJSON","readFileSync","files","propName","item","data","parsedData","JSON","stringify","deepCopy","copy","Array","isArray","key","prototype","hasOwnProperty","call","optionsStringify","allowFunctions","replaceAll","printUsage","bold","yellow","cycleCategories","option","entries","descName","green","i","blue","category","toUpperCase","red","toBoolean","wrapAround","replace","measureTime","start","hrtime","bigint","Number","generalOptions","getOptions","mergeConfigOptions","newOptions","mergedOptions","updateDefaultConfig","configObj","customObj","customValue","initOptions","items","recursiveProps","objectToUpdate","nestedNames","shift","assign","async","fetch","requestOptions","Promise","reject","protocol","https","http","getProtocol","get","headers","Referer","res","on","chunk","text","ExportError","Error","constructor","super","this","setError","statusCode","errorCode","setCode","cache","activeManifest","sources","hcVersion","extractVersion","indexOf","extractModuleName","scriptPath","fetchAndProcessScript","script","fetchedModules","shouldThrowError","response","resolvedScriptPath","sep","updateCache","highchartsOptions","proxyOptions","sourcePath","proxyAgent","HttpsProxyAgent","agent","all","c","m","fetchScripts","writeFileSync","checkAndUpdateCache","manifestPath","requestUpdate","manifest","moduleMap","numberOfModules","some","moduleName","newManifest","saveConfigToManifest","getCachePath","setupHighcharts","Highcharts","animObject","duration","triggerExport","chartOptions","displayErrors","_displayErrors","merge","setOptions","wrap","setOptionsObj","chart","animation","strInj","isRenderComplete","Chart","proceed","userOptions","cb","exporting","enabled","plotOptions","series","label","tooltip","onHighchartsRender","addEvent","Series","Function","finalOptions","finalCallback","defaultOptions","prop","template","browser","browserGeneration","lastPuppeteerArgs","closingOnPurpose","launchPromise","getGeneration","handleDisconnect","isAlive","proc","pid","kill","code","terminate","instance","close","onExit","clearTimeout","timer","setTimeout","removeListener","once","waitForExit","create","puppeteerArgs","connected","puppeteerOptions","enabledDebug","debugOptions","launchOptions","userDataDir","handleSIGINT","handleSIGTERM","handleSIGHUP","waitForInitialPage","defaultViewport","retryWindow","configured","parseInt","getLaunchRetryWindow","deadline","now","attempt","delay","launch","remaining","Math","ceil","random","launchBrowser","finally","newPage","page","setCacheEnabled","setPageContent","isClosed","$eval","element","errorMessage","innerHTML","setPageEvents","closeError","clearPageResources","injectedResources","resource","dispose","evaluate","oldCharts","charts","oldChart","destroy","scriptsToRemove","document","getElementsByTagName","stylesToRemove","linksToRemove","remove","setContent","waitUntil","addScriptTag","path","setAsConfig","totalSize","Buffer","byteLength","toFixed","puppeteerExport","exportOptions","debugger","isSVG","svgTemplate","injectedJs","js","push","content","isLocal","jsResource","injectedCss","css","cssImports","match","cssImportPath","cssResource","addStyleTag","addPageResources","size","svgElement","querySelector","chartHeight","baseVal","chartWidth","body","style","zoom","margin","viewportHeight","abs","viewportWidth","x","y","getBoundingClientRect","trunc","getClipRegion","setViewport","deviceScaleFactor","outerHTML","createSVG","encoding","clip","race","screenshot","captureBeyondViewport","fullPage","optimizeForSpeed","quality","omitBackground","_resolve","createImage","emulateMediaType","pdf","createPDF","errorCodes","stats","performedExports","exportAttempts","exportFromSvgAttempts","timeSpent","droppedExports","spentAverage","rejectedForCapacity","abandonedExports","poolConfig","consecutiveCreateFailures","getConsecutiveCreateFailures","getQueueLimit","factory","id","uuid","startDate","getTime","generation","getBrowserGeneration","workCount","validate","workerHandle","mustRecycle","cleanPromise","cleared","initPool","resolveQueueLimit","createBrowser","Pool","acquireTimeoutMillis","createTimeoutMillis","destroyTimeoutMillis","idleTimeoutMillis","createRetryIntervalMillis","reapIntervalMillis","propagateCreateError","hardReset","goto","clearPage","catch","eventId","initialResources","acquire","promise","release","killPool","worker","used","destroyed","closeBrowser","postWork","getPoolInfo","numPendingAcquires","payload","requestId","abortSignal","aborted","acquireCounter","workStart","exportCounter","result","exportTime","getPool","getPoolInfoJSON","numFree","numUsed","available","pending","pool$1","purifier","sanitize","forbidden","DOMPurify","JSDOM","ADD_TAGS","FORBID_ATTR","HTML_INTEGRATION_POINTS","foreignobject","startExport","settings","endCallback","svg","initExportSettings","exportAsString","doStraightInject","doExport","findChartSize","precision","multiplier","pow","roundNumber","sourceHeight","sourceWidth","param","chartJson","customLogicOptions","allowCodeExecutionScoped","optionsName","stringToExport","chartJSON","intervalIds","logErrorMiddleware","req","next","returnErrorMiddleware","headersSent","end","stCode","status","json","rateLimit","app","limitConfig","msg","rateOptions","limiter","windowMs","limit","handler","request","format","send","default","skip","query","access_token","use","HttpError","setStatus","vSwitchRoute","post","adminToken","token","newVersion","params","updateVersion","reversedMime","png","jpeg","gif","requestsCounter","beforeRequest","afterRequest","doCallbacks","callbacks","uniqueId","callResponse","exportHandler","stopCounter","connection","remoteAddress","b64","noDownload","abortController","AbortController","writableFinished","abort","toLowerCase","substr","signal","pattern","isPrivateRangeUrlFound","info","from","header","attachment","filename","pkgFile","pather","serverStartTime","successRates","calculateSuccessRatio","attempts","addHealthRoutes","setInterval","successRatio","_","period","movingAverage","reduce","a","b","bootTime","uptime","floor","highchartsVersion","averageProcessingTime","failedExports","browserConnected","sucessRatio","svgExportAttempts","jsonExportAttempts","activeServers","Map","express","disable","cors","_req","set","attachServerErrorHandlers","socket","applyServerTimeouts","serverConfig","keepAlive","headersTimeout","startServer","uploadLimitBytes","storage","multer","memoryStorage","upload","limits","fieldSize","method","poolStats","getQueueRejectDelay","onClose","urlencoded","extended","none","httpServer","createServer","listen","cert","fsPromises","readFile","posix","httpsServer","NaN","static","healthRoute","exportRoutes","_request","sendFile","acceptRanges","uiRoute","errorHandler","closeServers","delete","closeIdleConnections","getServers","enableRateLimiting","getExpress","getApp","middlewares","shutdownCleanUp","exitCode","clearInterval","clearAllIntervals","drainTimeout","getDrainTimeout","exit","index","initExport","loggingOptions","initLogging","singleExport","batchExport","batchFunctions","pair","configIndex","findIndex","arg","fileName","loadConfigFile","showUsage","propertiesChain","argumentType","pairArgumentValue","mapToNewConfig","oldOptions","manualConfig","configFileName","configFile","choice","prompts","onSubmit","p","categories","questionsCounter","allQuestions","section","prompt","answer","module","writeFile","printLogo","packageVersion"],"mappings":"oqBAeO,MAAMA,EAAe,CAC1BC,KAAM,CAAC,aAAc,kBAAmB,iBACxCC,QAAS,CACP,QACA,MACA,QACA,YACA,uBACA,gBAEA,eACA,QACA,OACA,aACA,mBACA,eACA,cACA,UACA,UACA,cACA,WACA,UACA,YACA,cACA,YACA,sBACA,SACA,SACA,WACA,aACA,YACA,eAEA,SACA,eACA,YACA,kBACA,SACA,cACA,mBACA,eACA,kBACA,cACA,eAEA,cACA,WACA,eACA,WACA,SACA,OACA,WACA,YACA,SACA,qBACA,aACA,WACA,WACA,WACA,WACA,eACA,UACA,kBACA,oBACA,aACA,UACA,cACA,YACA,YAEFC,WAAY,CAAC,kBACbC,OAAQ,CACN,wEACA,mGAMSC,EAAgB,CAC3BC,UAAW,CACTC,KAAM,CACJC,MAAO,CACL,mCACA,kBACA,0CACA,2BACA,kCACA,kCACA,wCACA,2CACA,qBACA,4BACA,2CACA,uDACA,6BACA,yBACA,0BACA,+BACA,uBACA,uFACA,yBACA,oCACA,oBACA,0BACA,8CACA,2BACA,0BACA,6BACA,mCACA,wCACA,mCACA,2BACA,kCACA,uBACA,iBACA,yBACA,8BACA,oBACA,2BACA,eACA,6BACA,iBACA,aACA,SAEA,sBAEA,yBACA,oBACA,uBAEFC,KAAM,WACNC,YAAa,yCAEfC,kBAAmB,CACjBH,MAAO,IACPC,KAAM,SACNG,QAAS,gCACTF,YACE,yTAEJG,QAAS,CACPL,MAAO,SACPC,KAAM,SACNG,QAAS,qBACTF,YAAa,0DAGjBI,WAAY,CACVC,QAAS,CACPP,MAAO,SACPC,KAAM,SACNG,QAAS,qBACTF,YAAa,sCAEfM,OAAQ,CACNR,MAAO,+BACPC,KAAM,SACNG,QAAS,qBACTF,YAAa,kDAEfO,OAAQ,CACNT,OAAO,EACPC,KAAM,UACNG,QAAS,qBACTF,YAAa,mDAEfQ,YAAa,CACXV,MAAOR,EAAaC,KACpBQ,KAAM,WACNG,QAAS,0BACTF,YAAa,yCAEfS,cAAe,CACbX,MAAOR,EAAaE,QACpBO,KAAM,WACNG,QAAS,4BACTF,YAAa,uCA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"} \ No newline at end of file