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

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,则需要检查密钥是否正确。

验证文件是否符合要求

  1. 检查文件完整性:用本地播放器打开MP3文件,确认能正常播放,排除文件损坏。
  2. 检查文件大小:Whisper支持最大25MB的音频文件,你的文件379KB,符合要求。
  3. 手动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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 12:27:06