Next.js(t3-app)调用OpenAI Whisper语音转文本Fetch失败求助
问题:T3 App + Next.js上传MP3到Whisper失败,提示请求内容长度不匹配
问题描述
使用T3 App搭配Next.js App Router开发,通过Server Action表单上传MP3文件到OpenAI Whisper语音转文本模型,但无法获取转录结果,报错提示请求内容长度不匹配。
组件代码
import { api } from "~/trpc/server"; export default function CreateTranscription() { async function create(formData: FormData) { "use server"; const audioFile = formData.get("audioFile"); // mutate data if (audioFile !== null && typeof audioFile !== "string") { const submit = await api.openai.speechToText.mutate({ audioFile: audioFile, }); } // revalidate cache } return ( <> <form action={create}> <input type="file" name="audioFile" id="audioFile" required /> <button type="submit">Upload</button> </form> </> ); }
TRPC路由代码
import { z } from "zod"; import { createTRPCRouter, publicProcedure, } from "~/server/api/trpc"; import OpenAI from "openai"; import type { Uploadable } from "openai/uploads"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); export const openAIRouter = createTRPCRouter({ speechToText: publicProcedure .input( z.object({ audioFile: z.custom<Uploadable>(), }), ) .mutation(async ({ input }) => { const transcriptions = await openai.audio.transcriptions.create({ file: input.audioFile, model: "whisper-1", }); return transcriptions; }), });
报错信息
input: { audioFile: File { size: 379924, type: 'audio/mpeg', name: 'Vaundysong.mp3', lastModified: 1700993541098 } }, result: TRPCClientError: fetch failed at TRPCClientError.from (webpack-internal:///(rsc)/./node_modules/@trpc/client/dist/TRPCClientError-0de4d231.mjs:41:16) at eval (webpack-internal:///(rsc)/./node_modules/@trpc/client/dist/httpBatchLink-204206a5.mjs:207:101) { meta: undefined, shape: undefined, data: undefined, [cause]: TypeError: fetch failed at Object.fetch (node:internal/deps/undici/undici:11576:11) { cause: [RequestContentLengthMismatchError] } }, elapsedMs: 8 } ⨯ node_modules/@trpc/client/dist/TRPCClientError-0de4d231.mjs (37:15) @ TRPCClientError.from ⨯ Internal error: TRPCClientError: fetch failed at TRPCClientError.from (./node_modules/@trpc/client/dist/TRPCClientError-0de4d231.mjs:41:16) at eval (./node_modules/@trpc/client/dist/httpBatchLink-204206a5.mjs:207:101) Cause: TypeError: fetch failed at Object.fetch (node:internal/deps/undici/undici:11576:11) { cause: RequestContentLengthMismatchError: Request body length does not match content-length header at AsyncWriter.end (/Users/hurraychak/Side Projects/muspic/node_modules/next/dist/compiled/undici/index.js:1:82741) at writeIterable (/Users/hurraychak/Side Projects/muspic/node_modules/next/dist/compiled/undici/index.js:1:81411) { code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' }
问题根源
这个报错的核心是TRPC无法正确序列化File对象,导致传输过程中请求体实际长度和Content-Length头不匹配,和OpenAI API密钥无关。TRPC默认基于JSON序列化数据,而File对象无法被JSON序列化,传递时会损坏数据。
验证API密钥有效性
方法1:通过TRPC测试
在你的TRPC路由中添加一个测试方法:
export const openAIRouter = createTRPCRouter({ // 原有speechToText方法 speechToText: publicProcedure... , // 新增测试密钥的方法 testApiKey: publicProcedure.mutation(async () => { try { const models = await openai.models.list(); return { success: true, modelCount: models.data.length }; } catch (err) { return { success: false, error: (err as Error).message }; } }) });
然后在Server Action中调用这个方法,查看返回结果:如果success为true,说明密钥有效;如果失败,根据错误信息排查(比如密钥无效、网络不通)。
方法2:终端命令测试
在终端执行以下命令(替换成你的OpenAI API密钥):
curl https://api.openai.com/v1/models -H "Authorization: Bearer YOUR_OPENAI_API_KEY"
如果返回模型列表JSON,说明密钥有效;如果返回invalid_api_key,则需要检查密钥是否正确。
验证文件是否符合要求
- 检查文件完整性:用本地播放器打开MP3文件,确认能正常播放,排除文件损坏。
- 检查文件大小:Whisper支持最大25MB的音频文件,你的文件379KB,符合要求。
- 手动API测试:用curl直接上传文件测试:
curl https://api.openai.com/v1/audio/transcriptions \ -H "Authorization: Bearer YOUR_OPENAI_API_KEY" \ -H "Content-Type: multipart/form-data" \ -F file="@Vaundysong.mp3" \ -F model="whisper-1"
如果返回转录结果,说明文件完全符合Whisper的要求。
代码修复方案
方案1:直接在Server Action中调用OpenAI API(最简单)
跳过TRPC,直接在Server Action里处理文件上传和API调用,避免序列化问题:
import OpenAI from "openai"; // 初始化OpenAI客户端 const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); export default function CreateTranscription() { async function create(formData: FormData) { "use server"; const audioFile = formData.get("audioFile"); if (audioFile !== null && typeof audioFile !== "string") { try { const transcript = await openai.audio.transcriptions.create({ file: audioFile, model: "whisper-1", }); console.log("转录结果:", transcript.text); // 这里可以添加后续逻辑,比如保存到数据库、重新验证缓存等 } catch (err) { console.error("转录失败:", err); } } } return ( <> <form action={create}> <input type="file" name="audioFile" id="audioFile" required /> <button type="submit">Upload</button> </form> </> ); }
方案2:通过TRPC传递Buffer(保留TRPC流程)
如果需要保留TRPC的调用流程,需要把File转成Buffer传递,避免序列化问题:
修改组件代码
import { api } from "~/trpc/server"; export default function CreateTranscription() { async function create(formData: FormData) { "use server"; const audioFile = formData.get("audioFile"); if (audioFile !== null && typeof audioFile !== "string") { // 将File转为Buffer const arrayBuffer = await audioFile.arrayBuffer(); const audioBuffer = Buffer.from(arrayBuffer); const transcript = await api.openai.speechToText.mutate({ audioBuffer, fileName: audioFile.name, mimeType: audioFile.type, }); console.log("转录结果:", transcript.text); } } return ( <> <form action={create}> <input type="file" name="audioFile" id="audioFile" required /> <button type="submit">Upload</button> </form> </> ); }
修改TRPC路由代码
import { z } from "zod"; import { createTRPCRouter, publicProcedure, } from "~/server/api/trpc"; import OpenAI from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); export const openAIRouter = createTRPCRouter({ speechToText: publicProcedure .input( z.object({ audioBuffer: z.instanceof(Buffer), fileName: z.string(), mimeType: z.string(), }), ) .mutation(async ({ input }) => { // 将Buffer转为Blob,适配OpenAI SDK要求 const audioBlob = new Blob([input.audioBuffer], { type: input.mimeType }); const transcript = await openai.audio.transcriptions.create({ file: audioBlob, model: "whisper-1", filename: input.fileName, }); return transcript; }), });
内容的提问来源于stack exchange,提问作者HurrayChak
相关产品推荐
相关产品推荐

