Next.js 13 App Router API路由中非阻塞Fetch请求无法触发问题
Next.js 13(App Router)Vercel生产环境中非阻塞Fetch请求无法触发的问题解决
问题原因分析
- Vercel的Serverless函数在执行完成(返回响应给客户端)后会立即终止进程,所有未完成的异步任务都会被直接中断。开发环境中Next.js的dev服务器不会立刻终止进程,因此未加
await的请求能继续执行;但生产环境的Serverless函数没有等待异步任务完成的机制,导致未await的请求还没来得及发送就被销毁。 - 你发起的
axios.post属于“fire-and-forget”的异步操作,但函数执行到return true后就结束了,Vercel会立刻销毁该函数的运行环境,请求自然无法触发。
解决方案
1. 消息队列(推荐方案,彻底解决长任务超时问题)
这是处理长异步任务的标准做法,既能让用户立即收到响应,又能规避Vercel的5分钟函数超时限制,流程如下:
- 主API路由拆分PDF后,将每个chunk的任务信息发送到消息队列;
- 单独创建Serverless函数作为队列处理器,从队列中取出任务执行embedding生成逻辑;
- 主路由直接返回响应告知用户任务已启动,用户可通过后续接口查询进度。
代码示例(以BullMQ为例)
首先安装依赖:
npm install bullmq
主函数修改:
import { Queue } from 'bullmq'; // 初始化队列,用Vercel KV作为Redis存储 const embeddingQueue = new Queue('embedding-generation', { connection: { host: process.env.KV_REST_API_URL, port: parseInt(process.env.KV_REST_API_PORT || '6379'), password: process.env.KV_REST_API_TOKEN, }, }); export default async function generateEmbeddingsForPDF( workspaceDataSource: WorkspaceFile, supabase: SupabaseClient<Database, 'public'>, request: NextRequest ): Promise<boolean> { const { data, error } = await supabase .storage.from('workspace-files') .download(workspaceDataSource.filePath); if (error && !data) { console.error('ERROR Downloading File::', error); return false; } try { const pdfLoader = new PDFLoader(data); const pageContentList = (await pdfLoader.loadAndSplit()).map(page => page.pageContent); const chunkSplitSize = 50; // 将所有chunk任务加入队列 for (let i = 0; i < pageContentList.length; i += chunkSplitSize) { const chunks = pageContentList.slice(i, i + chunkSplitSize); await embeddingQueue.add('process-chunk', { workspaceFile: workspaceDataSource, textChunks: chunks, totalChunks: pageContentList.length, cookie: request.headers.get('cookie'), }); } } catch (error) { console.error('Error splitting pdf into pages::', error); return false; } return true; }
创建队列处理器(/app/api-v1/workspaces/generate-embeddings/processor/route.ts):
import { Worker } from 'bullmq'; import axios from 'axios'; // 初始化任务处理器 const worker = new Worker('embedding-generation', async (job) => { const { workspaceFile, textChunks, totalChunks, cookie } = job.data; const url = `${process.env.NEXT_PUBLIC_APP_URL}/api-v1/workspaces/generate-embeddings/ingestor`; await axios.post(url, { workspaceFile, textChunks, totalChunks, }, { headers: { cookie, 'Content-Type': 'application/json', } }); }, { connection: { host: process.env.KV_REST_API_URL, port: parseInt(process.env.KV_REST_API_PORT || '6379'), password: process.env.KV_REST_API_TOKEN, }, }); // 监听任务失败事件 worker.on('failed', (job, err) => { console.error(`Job ${job.id} failed: ${err.message}`); }); // 路由仅用于初始化Worker export async function GET() { return new Response('Worker running', { status: 200 }); }
2. 临时方案:强制等待请求发送(不适合长任务)
如果暂时不想引入队列,可以用Promise.allSettled收集所有请求的Promise并等待,确保请求至少被发送出去,但主函数会等待所有请求启动后才返回,仍受Vercel超时限制,仅适合短任务:
// 替换原for循环部分 const requests = []; for (let i = 0; i < pageContentList.length; i += chunkSplitSize) { const chunks = pageContentList.slice(i, i + chunkSplitSize); const url = `${request.nextUrl.origin}/api-v1/workspaces/generate-embeddings/ingestor`; const reqPromise = axios.post(url, { workspaceFile: workspaceDataSource, textChunks: chunks, totalChunks: pageContentList.length, }, { headers: { cookie: request.headers.get('cookie'), 'Content-Type': 'application/json', } }).catch(err => { console.error('ERROR WITH EMBEDDINGS DISPATCHER::', err); }); requests.push(reqPromise); } // 等待所有请求完成发送(不一定执行完毕) await Promise.allSettled(requests);
内容的提问来源于stack exchange,提问作者Angus Moore
相关产品推荐
相关产品推荐

