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
相关产品推荐
相关产品推荐

