Azure函数应用解析含音频FormData问题求助(NextJS+SWA)
问题描述
我在Azure上开发基于Next.js的静态Web应用(SWA),需求是录制5-10秒语音并通过OpenAI生成转录文本。用Postman发送含音频数据的表单时代码正常运行,但在SWA中向Azure Functions v4发送含音频的FormData时,函数无法提取音频甚至获取不到FormData,无响应也无报错。
尝试过以下工具均遇问题:
- formidable:模块导入方式不兼容(require/var语法与ES模块冲突)
- @anzp/azure-function-multipart:依赖旧版@azure/functions@3.2.0,与当前v4.6.0不兼容,类型错误提示
HttpRequest缺少get、parseFormBody属性,patch无效 - busboy:初始出现
TypeError [ERR_INVALID_STATE]: Invalid state: The ReadableStream is locked错误,调整后虽能生成文件但文件不完整,播放时提示net::ERR_REQUEST_RANGE_NOT_SATISFIABLE
需求:在Azure Functions v4中正确解析FormData并提取音频文件,先存储到本地文件夹(测试用),后续存入Azure存储。
附原前后端代码:
前端代码:
async function formAction(event: any) { const NewForm = new FormData(); NewForm.append('audio', event.get('audio'), 'audio.webm'); console.log({auidFormData: event.get('audio'), event: NewForm}); const awaitTranscript = await fetch("/api/voiceTranscriptTrigger",{ method:"POST", body: NewForm, headers: { "Content-Type": "multipart/form-data" } }); if(awaitTranscript.ok){ const response = await awaitTranscript.json(); console.log({response_awaitTranscript: response}); } }
后端代码:
import parseMultipartFormData from "@anzp/azure-function-multipart"; export async function voiceTranscriptTrigger(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { try { console.log(`==> before checking request : <==`); const { fields, files } = await parseMultipartFormData(request); console.log({fields: fields}); console.log({files: files}); return new HttpResponse(test); } catch (e) { console.error("Error while trying to upload a file\n", e); return new HttpResponse({body: JSON.stringify({"error": "Something went wrong."})}); } }; app.http('voiceTranscriptTrigger', { methods: ['POST'], authLevel: 'anonymous', handler: voiceTranscriptTrigger });
解决方案
1. 前端代码修正
手动设置Content-Type: multipart/form-data会丢失浏览器自动生成的boundary参数,导致后端无法解析FormData。直接移除该headers配置:
async function formAction(event: any) { const formData = new FormData(); formData.append('audio', event.get('audio'), 'audio.webm'); const response = await fetch("/api/voiceTranscriptTrigger", { method: "POST", body: formData // 移除手动设置的Content-Type,让浏览器自动生成带boundary的请求头 }); if(response.ok){ const data = await response.json(); console.log({transcriptResponse: data}); } }
2. 后端用Busboy正确解析FormData(Azure Functions v4兼容版)
Busboy需要正确处理Azure Functions的HttpRequest流,避免流锁定问题。以下是包含本地文件存储逻辑的完整实现:
首先安装依赖:
npm install busboy
后端代码:
import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; import Busboy from "busboy"; import { createWriteStream, mkdirSync, existsSync } from "fs"; import { join } from "path"; export async function voiceTranscriptTrigger(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { return new Promise((resolve, reject) => { try { // 确保本地存储目录存在 const uploadDir = join(process.cwd(), "uploads"); if (!existsSync(uploadDir)) { mkdirSync(uploadDir, { recursive: true }); } const busboy = Busboy({ headers: request.headers as Record<string, string> }); let audioFilePath: string | null = null; // 处理文件字段 busboy.on("file", (fieldname, file, filename) => { if (fieldname === "audio" && filename) { audioFilePath = join(uploadDir, filename); const writeStream = createWriteStream(audioFilePath); file.pipe(writeStream); writeStream.on("finish", () => { context.log(`音频文件已保存到: ${audioFilePath}`); }); file.on("error", (err) => { context.error("文件流处理错误:", err); reject({ status: 500, body: JSON.stringify({ error: "文件处理失败" }) }); }); } }); // 处理表单字段(如果有) busboy.on("field", (fieldname, val) => { context.log(`表单字段 ${fieldname}: ${val}`); }); // 解析完成回调 busboy.on("finish", async () => { if (!audioFilePath) { return resolve({ status: 400, body: JSON.stringify({ error: "未找到音频文件" }) }); } // 此处可添加调用OpenAI生成转录文本的逻辑 resolve({ status: 200, body: JSON.stringify({ message: "音频上传成功", filePath: audioFilePath }) }); }); // 将Azure的ReadableStream传递给Busboy处理 request.body?.pipe(busboy); } catch (err) { context.error("解析FormData失败:", err); resolve({ status: 500, body: JSON.stringify({ error: "服务器内部错误" }) }); } }); } app.http('voiceTranscriptTrigger', { methods: ['POST'], authLevel: 'anonymous', handler: voiceTranscriptTrigger });
关键注意点
- 流处理:Azure Functions v4的
request.body是ReadableStream,需直接通过pipe传递给Busboy,避免多次读取导致流锁定 - 目录创建:使用
mkdirSync的recursive: true参数自动创建多级目录,确保存储路径存在 - 错误捕获:在文件流、Busboy解析流程中添加错误监听,避免无响应问题
后续扩展(存入Azure存储)
测试完成后,可替换本地存储逻辑为Azure Blob存储:
- 安装依赖:
npm install @azure/storage-blob - 替换本地存储代码为Blob上传逻辑:
import { BlobServiceClient } from "@azure/storage-blob"; // 在busboy的finish回调中替换本地存储逻辑 const blobServiceClient = BlobServiceClient.fromConnectionString(process.env.AZURE_STORAGE_CONNECTION_STRING!); const containerClient = blobServiceClient.getContainerClient("audio-uploads"); const blobClient = containerClient.getBlockBlobClient(filename); await blobClient.uploadFile(audioFilePath);
内容的提问来源于stack exchange,提问作者Tkay Bay
相关产品推荐
相关产品推荐

