Node.js路由传参all触发bigint类型错误,PDF生成失败求助
问题描述
我配置了一条Express路由,当请求路径传入all.pdf时,本应查询数据库中所有ID,为每个ID生成PDF并合并成单个多页PDF,但此时会报错:error: invalid input syntax for type bigint: "all"。传入带limit参数的路径(比如all/15.pdf)时能正常拉取对应数量的ID生成PDF,单独创建all.pdf的路由也会触发相同错误;但逻辑完全一致的Zip文件生成路由使用all参数却能正常运行。
错误信息
error: invalid input syntax for type bigint: "all" at Parser.parseErrorMessage (C:\Users\TI\Desktop\carta-api-mesquita\node_modules\pg-protocol\dist\parser.js:287:98) at Parser.handlePacket (C:\Users\TI\Desktop\carta-api-mesquita\node_modules\pg-protocol\dist\parser.js:126:29) at Parser.parse (C:\Users\TI\Desktop\carta-api-mesquita\node_modules\pg-protocol\dist\parser.js:39:38) at Socket.<anonymous> (C:\Users\TI\Desktop\carta-api-mesquita\node_modules\pg-protocol\dist\index.js:11:42) at Socket.emit (node:events:513:28) at addChunk (node:internal/streams/readable:324:12) at readableAddChunk (node:internal/streams/readable:297:9) at Readable.push (node:internal/streams/readable:234:10) at TCP.onStreamRead (node:internal/stream_base_commons:190:23) { length: 98, severity: 'ERROR', code: '22P02', detail: undefined, hint: undefined, position: '47', internalPosition: undefined, internalQuery: undefined, where: undefined, schema: undefined, table: undefined, column: undefined, dataType: undefined, constraint: undefined, file: 'int8.c', line: '124', routine: 'scanint8' } C:\Users\TI\Desktop\carta-api-mesquita\node_modules\puppeteer-core\lib\cjs\puppeteer\common\ExecutionContext.js:258 throw new Error('Evaluation failed: ' + (0, util_js_1.getExceptionMessage)(exceptionDetails)); ^ Error: Evaluation failed: TypeError: Cannot read properties of null (reading 'remove') at pptr://__puppeteer_evaluation_script__:3:16 at ExecutionContext._ExecutionContext_evaluate (C:\Users\TI\Desktop\carta-api-mesquita\node_modules\puppeteer-core\lib\cjs\puppeteer\common\ExecutionContext.js:258:15) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async ExecutionContext.evaluate (C:\Users\TI\Desktop\carta-api-mesquita\node_modules\puppeteer-core\lib\cjs\puppeteer\common\ExecutionContext.js:146:16) at async C:\Users\TI\Desktop\carta-api-mesquita\src\config\RenderPdf.js:17:5
PDF生成路由代码
router.get('/:database/:tipo/:ids/:limite?.pdf', async (req, res) => { const { database, tipo, ids, limite } = req.params; let idsArray; if (ids === "all") { const pool = new Pool({ user: process.env.POSTGRES_USER, host: process.env.POSTGRES_HOST, database: database, password: process.env.POSTGRES_PASSWORD, port: process.env.POSTGRES_PORT, }); let query = `SELECT DISTINCT id FROM controle_interno.${tipo} ORDER BY id ASC`; if (limite) { query += ` LIMIT ${limite}`; } const result = await pool.query(query); idsArray = result.rows.map(row => row.id); } else { idsArray = ids.split(","); } const pdfs = []; const browser = await puppeteer.launch(); const page = await browser.newPage(); for (let i = 0; i < idsArray.length; i++) { const id = idsArray[i]; const url = `http://localhost:${port}/${database}/${tipo}/${id}`; console.log(url); try { await page.goto(url, {waitUntil: 'networkidle0'}); await page.evaluate(() => { const button = document.getElementById('gerar-pdf'); button.remove(); }); const pdfBytes = await page.pdf({ format: 'A4', printBackground: true, pageRanges: '1' }); pdfs.push(await PDFDocument.load(pdfBytes)); } catch (err) { console.error(err.stack); } } await browser.close(); const mergedPdf = await PDFDocument.create(); for (const pdf of pdfs) { const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()); copiedPages.forEach((page) => mergedPdf.addPage(page)); } const pdfBytes = await mergedPdf.save(); const filePath = path.join(__dirname, 'relatorio.pdf'); await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); await fs.promises.writeFile(filePath, pdfBytes); res.set({ 'Content-Type': 'application/pdf', 'Content-Disposition': 'attachment; filename=relatorio.pdf', 'Content-Length': pdfBytes.length }); const stream = fs.createReadStream(filePath); stream.pipe(res); res.on('finish', async () => { // apaga o arquivo do diretório await fs.promises.unlink(filePath)}); });
Zip生成正常路由代码
router.get('/:database/:tipo/:ids/:limite?.zip', async (req, res) => { const { database, tipo, ids, limite } = req.params; let idsArray; if (ids === "all") { const pool = new Pool({ user: process.env.POSTGRES_USER, host: process.env.POSTGRES_HOST, database: database, password: process.env.POSTGRES_PASSWORD, port: process.env.POSTGRES_PORT, }); let query = `SELECT DISTINCT id FROM controle_interno.${tipo} ORDER BY id ASC`; if (limite) { query += ` LIMIT ${limite}`; } const result = await pool.query(query); idsArray = result.rows.map(row => row.id); } else { idsArray = ids.split(","); } const zip = new AdmZip(); const browser = await puppeteer.launch(); const page = await browser.newPage(); for (let i = 0; i < idsArray.length; i++) { const id = idsArray[i]; const url = `http://localhost:${port}/${database}/${tipo}/${id}`; console.log(url); try { await page.goto(url, {waitUntil: 'networkidle0'}); await page.evaluate(() => { const button = document.getElementById('gerar-pdf'); button.remove(); }); const pdf = await page.pdf({ format: 'A4', printBackground: true, pageRanges: '1' }); zip.addFile(`${id}.pdf`, pdf, `PDF para o ID ${id}`); } catch (err) { console.error(err.stack); } } await browser.close(); const zipBuffer = zip.toBuffer(); res.set({ 'Content-Type': 'application/zip', 'Content-Disposition': `attachment; filename=${tipo}.zip`, 'Content-Length': zipBuffer.length }); res.send(zipBuffer); });
问题分析与解决方案
核心问题
- 路由参数解析错误:原PDF路由
/:database/:tipo/:ids/:limite?.pdf的可选参数limite?导致请求/xxx/xxx/all.pdf时,Express会将all.pdf整体解析为ids参数的值,而非将all作为ids、.pdf作为后缀。后续循环中会把all.pdf当作ID请求单个PDF接口,触发数据库bigint类型转换错误。 - 按钮移除未做存在判断:Puppeteer执行
button.remove()时未检查按钮是否存在,页面无该按钮时直接抛出错误。
解决方案
1. 拆分PDF路由,避免参数混淆
将原路由拆分为两个,分别处理带limit和不带limit的场景,确保.pdf后缀不会被包含到参数中:
// 处理不带limit的请求:/database/tipo/all.pdf router.get('/:database/:tipo/:ids.pdf', async (req, res) => { const { database, tipo, ids } = req.params; await handlePdfGeneration(database, tipo, ids, undefined, res); }); // 处理带limit的请求:/database/tipo/all/15.pdf router.get('/:database/:tipo/:ids/:limite.pdf', async (req, res) => { const { database, tipo, ids, limite } = req.params; await handlePdfGeneration(database, tipo, ids, limite, res); }); // 提取公共逻辑到函数,减少重复代码 async function handlePdfGeneration(database, tipo, ids, limite, res) { let idsArray; if (ids === "all") { const pool = new Pool({ user: process.env.POSTGRES_USER, host: process.env.POSTGRES_HOST, database: database, password: process.env.POSTGRES_PASSWORD, port: process.env.POSTGRES_PORT, }); let query = `SELECT DISTINCT id FROM controle_interno.${tipo} ORDER BY id ASC`; if (limite) { query += ` LIMIT ${limite}`; } const result = await pool.query(query); idsArray = result.rows.map(row => row.id); } else { idsArray = ids.split(","); } const pdfs = []; const browser = await puppeteer.launch(); const page = await browser.newPage(); for (let i = 0; i < idsArray.length; i++) { const id = idsArray[i]; const url = `http://localhost:${port}/${database}/${tipo}/${id}`; console.log(url); try { await page.goto(url, {waitUntil: 'networkidle0'}); await page.evaluate(() => { const button = document.getElementById('gerar-pdf'); if (button) button.remove(); // 新增存在判断,避免报错 }); const pdfBytes = await page.pdf({ format: 'A4', printBackground: true, pageRanges: '1' }); pdfs.push(await PDFDocument.load(pdfBytes)); } catch (err) { console.error(err.stack); } } await browser.close(); const mergedPdf = await PDFDocument.create(); for (const pdf of pdfs) { const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()); copiedPages.forEach((page) => mergedPdf.addPage(page)); } const pdfBytes = await mergedPdf.save(); const filePath = path.join(__dirname, 'relatorio.pdf'); await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); await fs.promises.writeFile(filePath, pdfBytes); res.set({ 'Content-Type': 'application/pdf', 'Content-Disposition': 'attachment; filename=relatorio.pdf', 'Content-Length': pdfBytes.length }); const stream = fs.createReadStream(filePath); stream.pipe(res); res.on('finish', async () => { await fs.promises.unlink(filePath); }); }
2. 优化单个PDF路由的参数验证
如果存在单个PDF的路由(/:database/:tipo/:id),添加参数合法性校验,避免无效ID传入数据库:
router.get('/:database/:tipo/:id', async (req, res) => { const { id } = req.params; // 验证ID是否为纯数字 if (!/^\d+$/.test(id)) { return res.status(400).json({ error: "ID必须为有效数字" }); } // 后续数据库查询逻辑 });
3. 全局修复Puppeteer按钮移除逻辑
在所有用到button.remove()的地方(包括Zip路由),统一添加存在判断:
await page.evaluate(() => { const button = document.getElementById('gerar-pdf'); if (button) button.remove(); });
内容的提问来源于stack exchange,提问作者lucasmsqt
相关产品推荐
相关产品推荐

