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

Puppeteer生成PDF上传至Supabase后无法打开问题求助

问题:生成并上传的PDF文件损坏无法打开

我开发了一款简历定制应用,流程如下:

  • 用户上传简历与职位描述后,AI生成定制化的HTML格式简历
  • 通过Puppeteer将HTML转换为PDF文件,上传至Supabase Storage
  • 引导用户进入扫描页面下载简历

目前遇到的问题:下载后的PDF无法打开,无预览效果,且在Supabase后台查看时该PDF文件也存在损坏无法打开的情况。


相关代码

scan/[scanID]/page.tsx

"use client";
import Navbar from "@/components/navbar";
import { Button } from "@/components/ui/button";
import { Check } from "lucide-react";
import React, { useEffect, useState } from "react";
import { findScanById, generatePDF, uploadPDF } from "../actions";
import { redirect } from "next/navigation";
import { handleDownload } from "@/utils/handleDownload";
import { createClerkSupabaseClient } from "@/utils/supabase/client";

const ScanDetails = ({ params }: { params: { scanId: string } }) => {
  const [pdfBlob, setPdfBlob] = useState<Blob | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchPDF = async () => {
      try {
        const scan = await findScanById(params.scanId);
        console.log({ scan });

        if (scan.length === 0) {
          redirect("/dashboard");
          return;
        }

        const supabase = createClerkSupabaseClient();

        let { data: old, error } = await supabase.storage
          .from("resumes")
          .download(`scans/${params.scanId}.pdf`);

        if (!old) {
          const pdfBuffer = await generatePDF({
            htmlContent: scan[0].ai_output,
          });
          console.log("Trying to upload pdf:", pdfBuffer);
          await uploadPDF(params.scanId, pdfBuffer);

          ({ data: old, error } = await supabase.storage
            .from("resumes")
            .download(`scans/${params.scanId}.pdf`));
        }

        setPdfBlob(old);
        setLoading(false);
      } catch (error) {
        console.error("Failed to fetch or generate PDF:", error);
      }
    };

    fetchPDF();
  }, [params.scanId]);

  return (
    <div className="min-h-screen flex flex-col">
      <Navbar />
      <div className="w-full flex-1 flex items-center px-8">
        <div className="w-full flex gap-24">
          <div className="bg-white w-[30%] py-10 justify-between h-[40rem] rounded-xl flex flex-col items-center">
            <div className="text-center flex flex-col items-center">
              <div className="rounded-full w-20 h-20 bg-green-400 flex justify-center items-center">
                <Check className="text-white" size={24} />
              </div>
              <h1 className="text-3xl pt-8">
                Your resume is <br /> now tailored.
              </h1>
            </div>
            <Button onClick={() => handleDownload(pdfBlob)} disabled={loading}>
              Download Resume
            </Button>
          </div>
          <div
            id="resume-content"
            className="w-[70%] h-[40rem] rounded-xl border-2 border-gray-700 bg-white overflow-y-auto"
          ></div>
        </div>
      </div>
    </div>
  );
};

export default ScanDetails;

scan/actions.ts

"use server";
import { createClerkSupabaseClient } from "@/utils/supabase/server";
import puppeteer from "puppeteer";

export const findScanById = async (id: string) => {
  const supabase = await createClerkSupabaseClient();
  const { data, error } = await supabase.from("Tailors").select().eq("id", id);

  if (error) {
    throw error;
  }

  return data;
};

export const generatePDF = async ({ htmlContent }: { htmlContent: string }) => {
  const browser = await puppeteer.launch();

  const page = await browser.newPage();

  await page.setContent(htmlContent, { waitUntil: "domcontentloaded" });

  await page.emulateMediaType("screen");

  const pdfBuffer = await page.pdf({
    margin: { top: 0, right: 0, bottom: 0, left: 0 },

    printBackground: true,

    format: "A4",
  });

  await browser.close();

  return pdfBuffer;
};

export const uploadPDF = async (scanId: string, pdfBuffer: Buffer) => {
  const supabase = await createClerkSupabaseClient();

  const { data, error } = await supabase.storage
    .from("resumes")
    .upload(`scans/${scanId}.pdf`, pdfBuffer, {
      cacheControl: "3600",
      upsert: true,
      contentType: "application/pdf",
    });

  if (error) throw new Error(`Failed to upload PDF: ${error.message}`);

  return data;
};

utils/handleDownload.ts

export const handleDownload = (pdfBlob: Blob | null) => {
  if (pdfBlob) {
    const url = URL.createObjectURL(pdfBlob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "resume.pdf"; // The filename you want the file to have
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);

    // Release the object URL after download
    URL.revokeObjectURL(url);
  }
};

排查与解决方案

1. 修复Puppeteer内容加载不完整问题

当前page.setContent使用domcontentloaded仅等待DOM加载完成,若HTML包含异步资源(图片、字体),会导致PDF生成时资源未加载完全,文件损坏。

修改generatePDF中的内容加载配置:

await page.setContent(htmlContent, { waitUntil: "networkidle0" });

networkidle0会等待页面所有网络请求完成后再执行下一步,确保所有资源加载完毕。

2. 优化Supabase上传的Buffer处理

直接上传Buffer可能存在格式兼容问题,转为ReadableStream并指定内容长度可提升稳定性:

export const uploadPDF = async (scanId: string, pdfBuffer: Buffer) => {
  const supabase = await createClerkSupabaseClient();
  // 将Buffer转为ReadableStream
  const stream = new ReadableStream({
    start(controller) {
      controller.enqueue(pdfBuffer);
      controller.close();
    }
  });

  const { data, error } = await supabase.storage
    .from("resumes")
    .upload(`scans/${scanId}.pdf`, stream, {
      cacheControl: "3600",
      upsert: true,
      contentType: "application/pdf",
      contentLength: pdfBuffer.length // 明确指定内容长度
    });

  if (error) throw new Error(`Failed to upload PDF: ${error.message}`);
  return data;
};

3. 确保下载后的Blob格式正确

从Supabase下载的Blob可能存在MIME类型异常,手动指定类型重新构造:
在fetchPDF函数中修改Blob赋值逻辑:

if (old) {
  // 重新构造Blob,强制指定PDF类型
  const correctBlob = new Blob([old], { type: "application/pdf" });
  setPdfBlob(correctBlob);
}

4. 优化Puppeteer启动配置

无头模式下的默认配置可能导致生成异常,添加必要参数:

const browser = await puppeteer.launch({
  headless: "new", // 使用最新无头模式
  args: ["--no-sandbox", "--disable-setuid-sandbox"] // 解决权限限制(开发环境用,生产环境需调整安全策略)
});

5. 验证生成的PDF本身是否有效

在generatePDF中添加本地保存逻辑,确认Puppeteer生成的PDF是否正常:

import fs from "fs/promises";

// 在return pdfBuffer前添加
await fs.writeFile("/tmp/test-resume.pdf", pdfBuffer);
console.log("本地已保存PDF,检查是否可正常打开");

如果本地文件正常,说明问题出在上传/下载环节;如果本地文件也损坏,需检查AI生成的HTML是否存在语法错误或不兼容内容。


内容的提问来源于stack exchange,提问作者RakibulB

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 13:33:11