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

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存储:

  1. 安装依赖:npm install @azure/storage-blob
  2. 替换本地存储代码为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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 18:35:21