diff --git a/server/middleware/validateCanvasRequestMiddleware.js b/server/middleware/validateCanvasRequestMiddleware.js new file mode 100644 index 000000000..4a3bdc12f --- /dev/null +++ b/server/middleware/validateCanvasRequestMiddleware.js @@ -0,0 +1,20 @@ +const { isObject } = require("lodash"); + +const validateCanvasRequestMiddleware = (req, res, next) => { + const { w, h, values, keys, override } = req.body; + if (!values || !keys) { + return res.status(400).send("Missing required data"); + } + if (override && !isObject(override)) { + return res.status(400).send("Override must be an object"); + } + if (w && (!Number.isFinite(w) || w <= 0)) { + return res.status(400).send("Width must be a positive number"); + } + if (h && (!Number.isFinite(h) || h <= 0)) { + return res.status(400).send("Height must be a positive number"); + } + next(); +}; + +module.exports = validateCanvasRequestMiddleware; diff --git a/server/render/canvas-handler.js b/server/render/canvas-handler.js index 0af2a7f53..a0fd0a602 100644 --- a/server/render/canvas-handler.js +++ b/server/render/canvas-handler.js @@ -5,89 +5,120 @@ const logger = require("../utils/logger"); const { backgroundColors, borderColors } = require("./canvas-colors"); const { isObject, defaultsDeep, isNumber } = require("lodash"); +let isProcessing = false; +const requestQueue = []; + +const processCanvasRequest = async (req, res) => { + try { + const { w, h, values, keys, override } = req.body; + logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override }); + + // Set dimensions with defaults + const width = isNumber(w) ? w : 500; + const height = isNumber(h) ? h : 275; + + const configuration = { + type: "doughnut", + data: { + labels: keys, + datasets: [ + { + data: values, + backgroundColor: backgroundColors, + borderColor: borderColors, + borderWidth: 1 + } + ] + }, + options: { + animation: false, + devicePixelRatio: 4, + responsive: false, + maintainAspectRatio: true, + circumference: 180, + rotation: -90, + plugins: { + legend: { + labels: { + boxWidth: 20, + font: { + family: "'Montserrat'", + size: 10, + style: "normal", + weight: "normal" + } + }, + position: "left" + } + } + } + }; + + // If we have a valid override object, merge it with the default configuration object. + // This allows for you to override the default configuration with a custom one. + const defaults = () => { + if (!override || !isObject(override)) { + return configuration; + } + return defaultsDeep(override, configuration); + }; + + // Generate chart + let canvas = createCanvas(width, height); + let ctx = canvas.getContext("2d"); + let chart = new Chart(ctx, defaults()); + const result = canvas.toDataURL(); + + chart.destroy(); + canvas.width = 0; + canvas.height = 0; + ctx = null; + canvas = null; + chart = null; + + res.status(200).send(result); + } catch (error) { + if (chart) chart.destroy(); + if (canvas) { + canvas.width = 0; + canvas.height = 0; + } + ctx = null; + canvas = null; + chart = null; + + logger.log("inbound-canvas-creation", "error", "jsr", null, { error: error.message, stack: error.stack }); + res.status(500).send("Error generating canvas"); + } +}; + +const processNextInQueue = async () => { + if (requestQueue.length === 0) { + isProcessing = false; + return; + } + + const { req, res } = requestQueue.shift(); + await processCanvasRequest(req, res); + processNextInQueue(); +}; + exports.canvastest = function (req, res) { - //console.log("Incoming test request.", req); res.status(200).send("OK"); }; -exports.canvas = function (req, res) { - const { w, h, values, keys, override } = req.body; - //console.log("Incoming Canvas Request:", w, h, values, keys, override); - logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override }); - // Gate required values - if (!values || !keys) { - res.status(400).send("Missing required data"); +exports.canvas = async function (req, res) { + if (isProcessing) { + if (requestQueue.length >= 100) { + // Set a maximum queue size + return res.status(503).send("Server is busy. Please try again later."); + } + requestQueue.push({ req, res }); + logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length }); return; } - // Override must be an object if it exists - if (override && !isObject(override)) { - res.status(400).send("Override must be an object"); - return; - } - - // Set the default Width and Height - let [width, height] = [500, 275]; - - // Allow for custom width and height - if (isNumber(w)) { - width = w; - } - if (isNumber(h)) { - height = h; - } - - const configuration = { - type: "doughnut", - data: { - labels: keys, - datasets: [ - { - data: values, - backgroundColor: backgroundColors, - borderColor: borderColors, - borderWidth: 1 - } - ] - }, - options: { - devicePixelRatio: 4, - responsive: false, - maintainAspectRatio: true, - circumference: 180, - rotation: -90, - plugins: { - legend: { - labels: { - boxWidth: 20, - font: { - family: "'Montserrat'", - size: 10, - style: "normal", - weight: "normal" - } - }, - position: "left" - } - } - } - }; - - // If we have a valid override object, merge it with the default configuration object. - // This allows for you to override the default configuration with a custom one. - const defaults = () => { - if (!override || !isObject(override)) { - return configuration; - } - return defaultsDeep(override, configuration); - }; - - res.status(200).send( - (() => { - const canvas = createCanvas(width, height); - const ctx = canvas.getContext("2d"); - new Chart(ctx, defaults()); - return canvas.toDataURL(); - })() - ); + isProcessing = true; + await processCanvasRequest(req, res); + processNextInQueue(); }; diff --git a/server/routes/renderRoutes.js b/server/routes/renderRoutes.js index 13288d989..2657d6a76 100644 --- a/server/routes/renderRoutes.js +++ b/server/routes/renderRoutes.js @@ -2,10 +2,11 @@ const express = require("express"); const router = express.Router(); const { inlinecss } = require("../render/inlinecss"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const validateCanvasRequestMiddleware = require("../middleware/validateCanvasRequestMiddleware"); const { canvas } = require("../render/canvas-handler"); // Define the route for inline CSS rendering router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss); -router.post("/canvas", validateFirebaseIdTokenMiddleware, canvas); +router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasRequestMiddleware], canvas); module.exports = router;