Skip to content

New ServiceRoute for html to pdf conversion#222

Merged
HugoGresse merged 2 commits intomainfrom
service-route-html
Feb 12, 2026
Merged

New ServiceRoute for html to pdf conversion#222
HugoGresse merged 2 commits intomainfrom
service-route-html

Conversation

@HugoGresse
Copy link
Owner

@HugoGresse HugoGresse commented Feb 12, 2026

Note

Medium Risk
Introduces a new PDF-generation endpoint and refactors Puppeteer flow/shared settings; risk is mainly around runtime behavior (resource cleanup, PDF rendering differences) and handling untrusted HTML/URLs in a headless browser.

Overview
Adds a new authenticated POST /v1/pdf/convert-html route that converts an array of HTML strings into a single merged PDF response.

Refactors the existing URL conversion route to share DEFAULT_SETTINGS, settings-schema reuse (pdfSettingsSchema), and helper functions (mergeSettings, setupPage, generatePdfFromPage) for consistent viewport/timezone/PDF output, plus minor error/response cleanup (ensuring pages and the browser are closed and returning more specific error messages).

Written by Cursor Bugbot for commit 947e362. Configure here.

@HugoGresse HugoGresse self-assigned this Feb 12, 2026
Copilot AI review requested due to automatic review settings February 12, 2026 17:53
@github-actions
Copy link

github-actions bot commented Feb 12, 2026

Visit the preview URL for this PR (updated for commit 7b8855c):

(expires Thu, 19 Feb 2026 18:00:43 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 0c15c45ea5a4c54095387eacf30c3755c9260f22

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

@HugoGresse HugoGresse deployed to GitHub Action PR February 12, 2026 17:59 — with GitHub Actions Active
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds/extends the Service API PDF conversion functionality by introducing shared PDF settings utilities and a new endpoint to convert raw HTML strings to a merged PDF.

Changes:

  • Refactors PDF settings handling into shared defaults + merge helpers (DEFAULT_SETTINGS, mergeSettings, setupPage, generatePdfFromPage).
  • Reuses a dedicated TypeBox schema for PDF settings (pdfSettingsSchema) across routes.
  • Adds a new /v1/pdf/convert-html route to convert multiple HTML strings into a single merged PDF.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +258 to 262
const page = await setupPage(browser, mergedSettings)
await page.setContent(html, { waitUntil: 'networkidle0' })
const pdfBuffer = await generatePdfFromPage(page, mergedSettings)
await merger.add(pdfBuffer as Uint8Array)
await page.close()
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with /v1/pdf/convert, browser/page are only closed on the success path. If setContent, PDF generation, or merging throws, Chromium can be left running. Use try/finally to always close browser, and close each page in a per-iteration finally to avoid leaks.

Suggested change
const page = await setupPage(browser, mergedSettings)
await page.setContent(html, { waitUntil: 'networkidle0' })
const pdfBuffer = await generatePdfFromPage(page, mergedSettings)
await merger.add(pdfBuffer as Uint8Array)
await page.close()
let page: Page | null = null
try {
page = await setupPage(browser, mergedSettings)
await page.setContent(html, { waitUntil: 'networkidle0' })
const pdfBuffer = await generatePdfFromPage(page, mergedSettings)
await merger.add(pdfBuffer as Uint8Array)
} finally {
if (page) {
await page.close()
}
}

Copilot uses AI. Check for mistakes.
Comment on lines 177 to 179
if (!urls.length) {
return reply.status(400).send(
JSON.stringify({
error: 'At least one URL is required',
})
)
return reply.status(400).send(JSON.stringify({ error: 'At least one URL is required' }))
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These error responses are JSON-stringified but no content-type: application/json header is set here, so Fastify will typically treat the payload as text/plain. Consider sending an object (letting Fastify serialize) or explicitly setting the JSON content-type for consistency with the global error handler.

Copilot uses AI. Check for mistakes.
Comment on lines +240 to +242
},
async (request, reply) => {
const { htmlContents, settings } = request.body
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error response is JSON-stringified but no content-type: application/json header is set, so the response may be treated as plain text. Consider sending an object or explicitly setting the JSON content-type (consistent with fastifyErrorHandler).

Copilot uses AI. Check for mistakes.
Comment on lines 163 to 167
@@ -79,90 +167,101 @@ export const pdfRoute = (fastify: FastifyInstance, options: any, done: () => any
}),
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 200-response schema documents a JSON object, but the handler actually returns a binary PDF (Buffer) with Content-Type: application/pdf. This will make Swagger/OpenAPI docs and any generated clients incorrect. Update the route schema to describe a PDF/binary response (and content-type) or change the handler to return the documented JSON payload.

Copilot uses AI. Check for mistakes.
Comment on lines +191 to +195
const page = await setupPage(browser, mergedSettings)
await page.goto(url, { waitUntil: 'networkidle0' })
const pdfBuffer = await generatePdfFromPage(page, mergedSettings)
await merger.add(pdfBuffer as Uint8Array)
await page.close()
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The browser (and potentially the current page) will not be closed if an error occurs anywhere in the loop (e.g., navigation timeout, invalid URL, PDF generation failure). In a long-lived/serverless runtime this can leak Chromium processes and memory. Use a try/finally (or try { ... } finally { await browser.close() }) and ensure each page.close() runs in a per-iteration finally as well.

Suggested change
const page = await setupPage(browser, mergedSettings)
await page.goto(url, { waitUntil: 'networkidle0' })
const pdfBuffer = await generatePdfFromPage(page, mergedSettings)
await merger.add(pdfBuffer as Uint8Array)
await page.close()
let page: Page | null = null
try {
page = await setupPage(browser, mergedSettings)
await page.goto(url, { waitUntil: 'networkidle0' })
const pdfBuffer = await generatePdfFromPage(page, mergedSettings)
await merger.add(pdfBuffer as Uint8Array)
} finally {
if (page) {
try {
await page.close()
} catch {
// Ignore errors on page.close to avoid masking prior failures
}
}
}

Copilot uses AI. Check for mistakes.
landscape: settings.pdf.landscape,
margin: settings.pdf.margin,
})
return Buffer.from(pdfBuffer)
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

page.pdf() in Puppeteer returns a Buffer (which is already a Uint8Array). Wrapping it in new Uint8Array(pdfBuffer) will allocate and copy the entire PDF for every page, increasing CPU/memory usage. Return the Buffer directly (or keep it as Uint8Array without copying) and adjust the return type accordingly.

Suggested change
return Buffer.from(pdfBuffer)
return pdfBuffer

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +230
body: Type.Object({
htmlContents: Type.Array(Type.String(), { description: 'Array of HTML content strings' }),
settings: pdfSettingsSchema,
}),
response: {
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 200-response schema documents a JSON object, but this handler returns a binary PDF (Buffer) with Content-Type: application/pdf. Update the route schema to describe a PDF/binary response (and content-type) or change the handler to return the documented JSON payload.

Copilot uses AI. Check for mistakes.
@HugoGresse HugoGresse merged commit 42b4c17 into main Feb 12, 2026
5 checks passed
@HugoGresse HugoGresse deleted the service-route-html branch February 12, 2026 18:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants