Backend hardening: structured logging, timeouts, memory leak fixes, compression, XSS fix
Some checks failed
Deploy to Production / Deploy to Server (push) Failing after 20s

- Add pino structured logging with request IDs (X-Request-Id header)
- Add 30s timeout to acquirePage() and renderPdf/renderUrlPdf
- Add verification cache cleanup (every 15min) and rate limit cleanup (every 60s)
- Read version from package.json in health endpoint
- Add compression middleware
- Escape currency in templates (XSS fix)
- Add static asset caching (1h maxAge)
- Remove deprecated docker-compose version field
- Replace all console.log/error with pino logger
This commit is contained in:
OpenClaw 2026-02-16 08:27:42 +00:00
parent 4833edf44c
commit 9541ae1826
20 changed files with 319 additions and 74 deletions

View file

@ -1,4 +1,5 @@
import puppeteer, { Browser, Page } from "puppeteer";
import logger from "./logger.js";
const BROWSER_COUNT = parseInt(process.env.BROWSER_COUNT || "2", 10);
const PAGES_PER_BROWSER = parseInt(process.env.PAGES_PER_BROWSER || "8", 10);
@ -90,9 +91,19 @@ async function acquirePage(): Promise<{ page: Page; instance: BrowserInstance }>
return { page, instance: inst };
}
// All pages busy, queue
return new Promise((resolve) => {
waitingQueue.push({ resolve });
// All pages busy, queue with 30s timeout
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
const idx = waitingQueue.findIndex((w) => w.resolve === resolve);
if (idx >= 0) waitingQueue.splice(idx, 1);
reject(new Error("QUEUE_FULL"));
}, 30_000);
waitingQueue.push({
resolve: (v) => {
clearTimeout(timer);
resolve(v);
},
});
});
}
@ -125,7 +136,7 @@ function releasePage(page: Page, inst: BrowserInstance): void {
async function scheduleRestart(inst: BrowserInstance): Promise<void> {
if (inst.restarting) return;
inst.restarting = true;
console.log(`Scheduling browser ${inst.id} restart (pdfs=${inst.pdfCount}, uptime=${Math.round((Date.now() - inst.lastRestartTime) / 1000)}s)`);
logger.info(`Scheduling browser ${inst.id} restart (pdfs=${inst.pdfCount}, uptime=${Math.round((Date.now() - inst.lastRestartTime) / 1000)}s)`);
const drainCheck = () => new Promise<void>((resolve) => {
const check = () => {
@ -159,7 +170,7 @@ async function scheduleRestart(inst: BrowserInstance): Promise<void> {
inst.pdfCount = 0;
inst.lastRestartTime = Date.now();
inst.restarting = false;
console.log(`Browser ${inst.id} restarted successfully`);
logger.info(`Browser ${inst.id} restarted successfully`);
while (waitingQueue.length > 0 && inst.availablePages.length > 0) {
const waiter = waitingQueue.shift();
@ -193,7 +204,7 @@ export async function initBrowser(): Promise<void> {
const inst = await launchInstance(i);
instances.push(inst);
}
console.log(`Browser pool ready (${BROWSER_COUNT} browsers × ${PAGES_PER_BROWSER} pages = ${BROWSER_COUNT * PAGES_PER_BROWSER} total)`);
logger.info(`Browser pool ready (${BROWSER_COUNT} browsers × ${PAGES_PER_BROWSER} pages = ${BROWSER_COUNT * PAGES_PER_BROWSER} total)`);
}
export async function closeBrowser(): Promise<void> {
@ -221,20 +232,26 @@ export async function renderPdf(
): Promise<Buffer> {
const { page, instance } = await acquirePage();
try {
await page.setContent(html, { waitUntil: "domcontentloaded", timeout: 15_000 });
await page.addStyleTag({ content: "* { margin: 0; padding: 0; } body { margin: 0; }" });
const pdf = await page.pdf({
format: (options.format as any) || "A4",
landscape: options.landscape || false,
printBackground: options.printBackground !== false,
margin: options.margin || { top: "0", right: "0", bottom: "0", left: "0" },
headerTemplate: options.headerTemplate,
footerTemplate: options.footerTemplate,
displayHeaderFooter: options.displayHeaderFooter || false,
});
return Buffer.from(pdf);
const result = await Promise.race([
(async () => {
await page.setContent(html, { waitUntil: "domcontentloaded", timeout: 15_000 });
await page.addStyleTag({ content: "* { margin: 0; padding: 0; } body { margin: 0; }" });
const pdf = await page.pdf({
format: (options.format as any) || "A4",
landscape: options.landscape || false,
printBackground: options.printBackground !== false,
margin: options.margin || { top: "0", right: "0", bottom: "0", left: "0" },
headerTemplate: options.headerTemplate,
footerTemplate: options.footerTemplate,
displayHeaderFooter: options.displayHeaderFooter || false,
});
return Buffer.from(pdf);
})(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("PDF_TIMEOUT")), 30_000)
),
]);
return result;
} finally {
releasePage(page, instance);
}
@ -252,19 +269,25 @@ export async function renderUrlPdf(
): Promise<Buffer> {
const { page, instance } = await acquirePage();
try {
await page.goto(url, {
waitUntil: (options.waitUntil as any) || "networkidle0",
timeout: 30_000,
});
const pdf = await page.pdf({
format: (options.format as any) || "A4",
landscape: options.landscape || false,
printBackground: options.printBackground !== false,
margin: options.margin || { top: "0", right: "0", bottom: "0", left: "0" },
});
return Buffer.from(pdf);
const result = await Promise.race([
(async () => {
await page.goto(url, {
waitUntil: (options.waitUntil as any) || "networkidle0",
timeout: 30_000,
});
const pdf = await page.pdf({
format: (options.format as any) || "A4",
landscape: options.landscape || false,
printBackground: options.printBackground !== false,
margin: options.margin || { top: "0", right: "0", bottom: "0", left: "0" },
});
return Buffer.from(pdf);
})(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("PDF_TIMEOUT")), 30_000)
),
]);
return result;
} finally {
releasePage(page, instance);
}