Sparticuz chromium-min working with vercel for pdf

Hi all,

I’m encountering a persistent runtime error when trying to generate PDFs using Puppeteer within a Next.js App Router project deployed on Vercel. The PDF generation works correctly in my local development environment but fails consistently on Vercel deployments.

Project Details:

  • **Project Name: Vectorcv
  • Framework: Next.js (App Router)
  • Node.js Runtime: Node 22.x (as per deployment logs)
  • **error from vercel
Error in PDF generation: Error: The input directory "/var/task/.next/server/bin" does not exist.
    at d.executablePath (/var/task/.next/server/chunks/191.js:1:64134)
    at l (/var/task/.next/server/app/api/pdf/route.js:1:1697)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async u (/var/task/.next/server/app/api/pdf/route.js:1:4235)
    at async /var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:38411
    at async e_.execute (/var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:27880)
    at async e_.handle (/var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:39943)
    at async en (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:16:25516)
    at async ea.responseCache.get.routeKind (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:17:1028)
    at async r9.renderToResponseWithComponentsImpl (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:17:508)


Error in PDF API route: Error: Failed to generate PDF: The input directory "/var/task/.next/server/bin" does not exist.
    at l (/var/task/.next/server/app/api/pdf/route.js:1:3486)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async u (/var/task/.next/server/app/api/pdf/route.js:1:4235)
    at async /var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:38411
    at async e_.execute (/var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:27880)
    at async e_.handle (/var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:39943)
    at async en (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:16:25516)
    at async ea.responseCache.get.routeKind (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:17:1028)
    at async r9.renderToResponseWithComponentsImpl (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:17:508)
    at async r9.renderPageComponent (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:17:5102)

Problem Description: The /api/pdf serverless function is designed to generate a PDF from HTML content using puppeteer-core and @sparticuz/chromium-min. While this works locally, on Vercel it fails with the following runtime error logged in the function logs:

Error: Failed to generate PDF: The input directory "/var/task/.next/server/bin" does not exist.
    at d.executablePath (/var/task/.next/server/chunks/191.js:1:64134)
    at l (/var/task/.next/server/app/api/pdf/route.js:1:1697)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async u (/var/task/.next/server/app/api/pdf/route.js:1:4235)
    ... (rest of stack trace) ...

This indicates that the necessary Chromium binary files provided by @sparticuz/chromium-min are not being found at the expected path within the Vercel runtime environment.

Troubleshooting Steps Taken:

  1. Dependencies: Confirmed use of puppeteer-core and @sparticuz/chromium-min (switched from the full @sparticuz/chromium in an attempt to resolve).
  2. Dynamic Imports: Implemented dynamic imports (await import(...)) for both puppeteer-core and @sparticuz/chromium-min within the service function (lib/services/pdf.service.ts) that handles PDF generation. This successfully reduced the reported Vercel function size from ~68MB to ~7MB, suggesting the initial bundle size limit is not the issue.
  3. next.config.mjs Configurations: Tried various configurations, including:
  • The recommended setup: experimental: { serverComponentsExternalPackages: ['@sparticuz/chromium-min'], outputFileTracingIncludes: { '/api/pdf': ['./node_modules/@sparticuz/chromium-min/**'] } }
  • Using more specific paths in outputFileTracingIncludes (e.g., targeting the bin directory).
  • Removing @sparticuz/chromium-min from serverComponentsExternalPackages.
  • Removing outputFileTracingIncludes entirely.
  • Using @vercel/nft’s withFileTracing helper.
  • Removing the entire experimental block related to tracing/externals. None of these configuration changes resolved the runtime path error.
  1. Environment Variables: Added SPARTICUZ_FUNCTION_NAME=pdf-generator to Vercel environment variables based on troubleshooting guides. Confirmed other potentially conflicting variables like PUPPETEER_EXECUTABLE_PATH are not set.
  2. Code Structure: The API route (app/api/pdf/route.tsx) simply imports and calls a service function (lib/services/pdf.service.ts). The service function contains the dynamic imports and Puppeteer logic, correctly calling await chromium.executablePath().

Despite these steps, the runtime error persists across deployments. It seems like an issue with Vercel’s build process not correctly tracing or packaging the necessary binary files from @sparticuz/chromium-min into the expected runtime location (/var/task/.next/server/bin or similar path resolved by chromium.executablePath()).

Could you please provide guidance on how to resolve this file tracing/packaging issue for @sparticuz/chromium-min within the Vercel environment?

also having @sparticuz/chromium-min has helped me keep this under 50MB limit

any assistance will be greatly appreciated. thank you.

Hey Levelz3111 :smiley:

I was able to take inspiration from this post on medium and get a working deployment on Vercel.

The big difference is it utlizes the remote executable vs trying to find the one if the file system from the dependency.

I’m not sure if that will give you what you need, my example just took a PDF of a site and returns it as a buffer. I’ll add the relevant bits below, curious to know your thoughts:

// package.json
"dependencies": {
    "@sparticuz/chromium-min": "^133.0.0",
    "next": "15.2.4",
    "puppeteer-core": "^24.5.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },

The Next Config

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
// v15 made this property stable
  serverExternalPackages: ["puppeteer-core", "@sparticuz/chromium"],
};

export default nextConfig;

Here is the main pdf service I have

// @/lib/pdf.ts
import chromium from "@sparticuz/chromium-min";
import puppeteerCore from "puppeteer-core";

async function getBrowser() {
  const REMOTE_PATH = process.env.CHROMIUM_REMOTE_EXEC_PATH;
  const LOCAL_PATH = process.env.CHROMIUM_LOCAL_EXEC_PATH;
  if (!REMOTE_PATH && !LOCAL_PATH) {
    throw new Error("Missing a path for chromium executable");
  }

  if (!!REMOTE_PATH) {
    return await puppeteerCore.launch({
      args: chromium.args,
      executablePath: await chromium.executablePath(
        process.env.CHROMIUM_REMOTE_EXEC_PATH,
      ),
      defaultViewport: null,
      headless: true,
    });
  }

  return await puppeteerCore.launch({
    executablePath: LOCAL_PATH,
    defaultViewport: null,
    headless: true,
  });
}

export const makePDFFromDomain = async (url: string): Promise<Buffer> => {
  try {
    const browser = await getBrowser();
    const page = await browser.newPage();

    page.on("pageerror", (err: Error) => {
      throw err;
    });
    page.on("error", (err: Error) => {
      throw err;
    });

    await page.goto(url);
    await page.setViewport({ width: 1080, height: 1024 });

    const pdf = await page.pdf({
      format: "A4",
      printBackground: true,
      margin: { top: "0", right: "0", bottom: "0", left: "0" },
      preferCSSPageSize: true,
      displayHeaderFooter: false,
      scale: 1.0,
    });

    return Buffer.from(pdf);
  } catch (error) {
    throw error;
  }
};

Please let me know if any of this helped, happy to keep trying to find a solution for you!

3 Likes