Сценарий · Inquir Compute

Обработка PDF: async-генерация и извлечение данных

Генерируйте PDF из HTML-шаблонов через Puppeteer (Node.js) или reportlab и WeasyPrint (Python), сохраняйте результат в объектное хранилище и отдавайте клиенту pre-signed URL — без HTTP-таймаута и без передачи бинарного файла в теле ответа.

Обновлено: 2026-06-28

Суть ответа

Обработка PDF: async-генерация и извлечение данных. HTTP-функция принимает параметры и сразу возвращает { jobId }, а пайплайн рендерит PDF (Puppeteer в Node.js или reportlab в Python), загружает его в S3 и возвращает pre-signed URL.

Когда подходит и когда нет

  • Генерация документа занимает больше двух секунд
  • Документ нужно хранить и скачивать несколько раз
  • Batch PDF generation (N invoices, N reports) with fan-out parallelism

На что обратить внимание

  • Пока генерация идёт в том же запросе, холодный старт контейнера с Puppeteer съедает значительную часть окна до таймаута шлюза.
  • Встраивание бинарника в JSON-ответ тоже не спасает: base64 раздувает payload, а клиент не может начать скачивание до получения всего тела.

Почему генерация PDF не помещается в синхронный HTTP

  • Рендер в Puppeteer или WeasyPrint занимает секунды — таймаут шлюза обрывает генерацию на середине
  • Передавать бинарный PDF в теле HTTP-ответа неудобно: клиент ждёт весь файл до закрытия соединения
  • Холодный старт контейнера с Puppeteer съедает значительную часть окна до таймаута

Рендер в Puppeteer или WeasyPrint занимает секунды, а HTTP-ответ с готовым PDF блокирует соединение всё это время — таймаут шлюза обрывает генерацию.

Почему синхронная генерация PDF в HTTP хрупка

Пока генерация идёт в том же запросе, холодный старт контейнера с Puppeteer съедает значительную часть окна до таймаута шлюза.

Встраивание бинарника в JSON-ответ тоже не спасает: base64 раздувает payload, а клиент не может начать скачивание до получения всего тела.

PDF-операции как шаги async-пайплайна

HTTP-функция принимает параметры и сразу возвращает { jobId }, а пайплайн рендерит PDF (Puppeteer в Node.js или reportlab в Python), загружает его в S3 и возвращает pre-signed URL.

Puppeteer подключается через слой: нативный бинарник Chromium доступен в контейнере Node.js 22 без собственного Dockerfile.

Что нужно для надёжной PDF-обработки

Разделение HTTP и долгой генерации

HTTP-функция сразу возвращает идентификатор задачи, а пайплайн генерирует PDF вне окна синхронного запроса — с собственным бюджетом памяти и ретраями при сбоях.

Объектное хранилище

Готовый PDF сохраняется в S3/GCS, а клиент получает pre-signed URL для скачивания — вместо бинарного блоба в теле ответа.

Нативные зависимости через слои

Puppeteer, Chromium, reportlab и WeasyPrint подключаются через слои — без сборки собственного Dockerfile.

Прослеживаемость по jobId

Идентификатор задачи ведёт к истории прогона и логам каждого шага, поэтому ошибки рендера проще отлаживать.

Как организовать PDF-генерацию

1

Принять запрос, вернуть 202

HTTP-функция валидирует параметры, запускает global.durable.startNew('render-invoice-pdf', undefined, { invoiceId, customerId }) и возвращает { invoiceId, status: 'generating' } с кодом 202.

2

Сгенерировать PDF в пайплайне

Puppeteer рендерит HTML в PDF, а reportlab строит документ программно; готовый файл сохраняется в S3.

3

Вернуть pre-signed URL

Пайплайн сохраняет URL в базе или отправляет вебхук; клиент получает ссылку и скачивает файл напрямую из S3.

Async-генерация PDF-счёта

HTTP-функция ставит работу через global.durable.startNew, запуская рендерер как durable-инстанс; эта функция рендерит HTML в PDF, загружает его в объектное хранилище и обновляет запись задачи. Инстанс переживает рестарты и адресуется по id для опроса статуса.

api/generate-invoice.mjs (HTTP handler)
export async function handler(event) {
  const { invoiceId, customerId } = JSON.parse(event.body || '{}');
  if (!invoiceId) return { statusCode: 400, body: JSON.stringify({ error: 'invoiceId required' }) };
  const existing = await db.invoicePdfs.find(invoiceId);
  if (existing?.url) return { statusCode: 200, body: JSON.stringify({ url: existing.url }) };
  await global.durable.startNew('render-invoice-pdf', undefined, { invoiceId, customerId });
  return { statusCode: 202, body: JSON.stringify({ invoiceId, status: 'generating' }) };
}
jobs/render-invoice-pdf.mjs (durable function)
import puppeteer from 'puppeteer-core';

export async function handler(event) {
  const { invoiceId, customerId } = event; // the input passed to startNew()
  const invoice = await db.invoices.findById(invoiceId);
  const html = renderInvoiceTemplate(invoice);
  const browser = await puppeteer.launch({ executablePath: '/usr/bin/chromium' });
  const page = await browser.newPage();
  await page.setContent(html, { waitUntil: 'networkidle0' });
  const pdfBytes = await page.pdf({ format: 'A4', printBackground: true });
  await browser.close();
  const url = await storage.upload(pdfBytes, `invoices/${invoiceId}.pdf`);
  await db.invoicePdfs.upsert({ invoiceId, url, generatedAt: new Date() });
  return { invoiceId, url };
}

Когда нужен async PDF

Когда это уместно

  • Генерация документа занимает больше двух секунд
  • Документ нужно хранить и скачивать несколько раз
  • Batch PDF generation (N invoices, N reports) with fan-out parallelism

Когда лучше выбрать другое

  • Простые одностраничные PDF без изображений, которые стабильно генерируются быстрее секунды, — оставьте их синхронными

Вопросы и ответы

Как добавить Puppeteer в Inquir?

Подключите слой с puppeteer-core и chromium-min к функции на Node.js 22 — после этого Puppeteer доступен через import puppeteer from "puppeteer-core".

Python или Node.js для PDF?

Node.js с Puppeteer удобен для рендера HTML в PDF; Python с reportlab или WeasyPrint — для программной генерации документов. Обе связки подключаются через слои.