You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
  });
问题分析与解决方案

核心问题

  1. 路由参数解析错误:原PDF路由/:database/:tipo/:ids/:limite?.pdf的可选参数limite?导致请求/xxx/xxx/all.pdf时,Express会将all.pdf整体解析为ids参数的值,而非将all作为ids、.pdf作为后缀。后续循环中会把all.pdf当作ID请求单个PDF接口,触发数据库bigint类型转换错误。
  2. 按钮移除未做存在判断: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 05:07:36