Next.js调用Azure文本转语音API出现ERR_REQUEST_RANGE_NOT_SATISFIABLE错误
问题描述
我使用Microsoft Azure文本转语音API,需求是点击按钮时在浏览器播放合成语音。通过Next.js API路由向Azure发起请求,客户端按钮调用该路由播放音频时出现以下错误:
blob:http://localhost:3000/aab03e2a-14c1-48a7-9dae-4eac158325a5:1
GET blob:http://localhost:3000/aab03e2a-14c1-48a7-9dae-4eac158325a5
net::ERR_REQUEST_RANGE_NOT_SATISFIABLElocalhost/:1 Uncaught (in promise) DOMException: Failed to load because no supported source was found.
相关代码如下:
/pages/api/synthesizeSpeech.tsx
import { NextApiRequest, NextApiResponse } from "next"; import * as sdk from "microsoft-cognitiveservices-speech-sdk"; export default async (req: NextApiRequest, res: NextApiResponse) => { if (req.method !== "POST") { return res.status(405).end(); } const speechConfig = sdk.SpeechConfig.fromSubscription(process.env.SPEECH_KEY, process.env.SPEECH_REGION); speechConfig.speechSynthesisVoiceName = "en-US-JennyNeural"; // Create a pull stream const pullStream = sdk.AudioOutputStream.createPullStream(); const audioConfig = sdk.AudioConfig.fromStreamOutput(pullStream); const synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig); const text = req.body.text; synthesizer.speakTextAsync( text, (result) => { if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) { // Set the appropriate headers for audio data res.setHeader("Content-Type", "audio/wav"); res.setHeader("Content-Disposition", "attachment; filename=speech.wav"); // Read the audio data from the pull stream and write it to the response const audioBuffer = []; const bufferSize = 10240; const buffer = new ArrayBuffer(bufferSize); let bytesRead = 0; do { // @ts-ignore bytesRead = pullStream.read(buffer); for (let i = 0; i < bytesRead; i++) { // @ts-ignore audioBuffer.push(buffer[i]); } } while (bytesRead > 0); res.status(200).end(Buffer.from(audioBuffer)); } else { res.status(500).json({ error: `Speech synthesis canceled, ${result.errorDetails}\nDid you set the speech resource key and region values?`, }); } synthesizer.close(); }, (err) => { res.status(500).json({ error: `Error - ${err}` }); synthesizer.close(); } ); };
pages/demo.tsx
const ButtonPanel = () => { const handleSynthesize = async (text: string) => { alert(text); try { const response = await fetch("/api/synthesizeSpeech", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ text }), }); if (!response.ok) { throw new Error("Failed to synthesize speech"); } const blob = await response.blob(); const audioUrl = URL.createObjectURL(blob); const audio = new Audio(audioUrl); audio.play(); } catch (error) { console.error(error); } }; return ( <footer className="m-4 mt-0 w-[calc(100vw-2rem)] rounded-b-lg border-t-2 border-gray-200 bg-white shadow-lg"> <div className="flex items-center justify-center space-x-4 p-4"> <button onClick={() => { handleSynthesize("hello this is a test test hello"); }} className="btn-solid w-32 disabled:cursor-not-allowed disabled:bg-gray-100" > <FaCircleArrowUp size={28} /> </button> </div> </footer> ); }
解决方案
问题根源
- 音频数据读取错误:手动读取pull stream并转换数组的方式会破坏WAV文件格式,导致浏览器无法识别。
- 缺少必要响应头:未设置
Content-Length,浏览器无法处理音频范围请求,触发ERR_REQUEST_RANGE_NOT_SATISFIABLE错误。 - 异步逻辑不规范:Next.js API路由中使用回调式异步方法,未正确处理Promise,可能导致响应提前终止。
修复代码
1. 修正API路由(/pages/api/synthesizeSpeech.tsx)
直接使用SDK返回的完整音频Buffer,规范异步处理并补充响应头:
import { NextApiRequest, NextApiResponse } from "next"; import * as sdk from "microsoft-cognitiveservices-speech-sdk"; export default async (req: NextApiRequest, res: NextApiResponse) => { if (req.method !== "POST") { return res.status(405).end(); } let synthesizer: sdk.SpeechSynthesizer | undefined; try { const speechConfig = sdk.SpeechConfig.fromSubscription( process.env.SPEECH_KEY!, process.env.SPEECH_REGION! ); speechConfig.speechSynthesisVoiceName = "en-US-JennyNeural"; synthesizer = new sdk.SpeechSynthesizer(speechConfig, undefined); const text = req.body.text; // 用Promise封装SDK异步方法,适配Next.js async/await const result = await new Promise<sdk.SpeechSynthesisResult>((resolve, reject) => { synthesizer!.speakTextAsync(text, resolve, reject); }); if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) { const audioBuffer = result.audioData; // 设置必要响应头 res.setHeader("Content-Type", "audio/wav"); res.setHeader("Content-Length", audioBuffer.length.toString()); // 设为inline让浏览器直接播放而非下载 res.setHeader("Content-Disposition", "inline; filename=speech.wav"); res.status(200).send(audioBuffer); } else { res.status(500).json({ error: `语音合成取消:${result.errorDetails}。请检查你的语音服务密钥和区域配置。`, }); } } catch (err) { res.status(500).json({ error: `合成失败:${(err as Error).message}` }); } finally { synthesizer?.close(); } };
2. 优化客户端播放逻辑(pages/demo.tsx)
指定Blob类型并添加错误、内存回收处理:
const ButtonPanel = () => { const handleSynthesize = async (text: string) => { try { const response = await fetch("/api/synthesizeSpeech", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ text }), }); if (!response.ok) { throw new Error(`请求失败,状态码:${response.status}`); } const blob = await response.blob(); // 明确指定Blob的MIME类型 const audioBlob = new Blob([blob], { type: "audio/wav" }); const audioUrl = URL.createObjectURL(audioBlob); const audio = new Audio(audioUrl); // 添加错误监听 audio.addEventListener("error", (e) => { console.error("音频播放错误:", e); }); await audio.play(); // 播放结束后释放Blob URL,避免内存泄漏 audio.addEventListener("ended", () => { URL.revokeObjectURL(audioUrl); }); } catch (error) { console.error("合成或播放失败:", error); } }; return ( <footer className="m-4 mt-0 w-[calc(100vw-2rem)] rounded-b-lg border-t-2 border-gray-200 bg-white shadow-lg"> <div className="flex items-center justify-center space-x-4 p-4"> <button onClick={() => handleSynthesize("hello this is a test test hello")} className="btn-solid w-32 disabled:cursor-not-allowed disabled:bg-gray-100" > <FaCircleArrowUp size={28} /> </button> </div> </footer> ); };
关键说明
- 直接使用
result.audioData获取SDK生成的完整音频Buffer,避免手动读取流导致的数据损坏。 - 添加
Content-Length头,让浏览器知晓音频文件大小,支持范围请求。 - 将
Content-Disposition设为inline,告知浏览器直接播放音频而非触发下载。 - 用Promise封装SDK异步回调,适配Next.js的async/await模式,避免回调逻辑混乱。
- 客户端明确指定Blob的MIME类型,并在播放结束后释放Blob URL,减少内存占用。
内容的提问来源于stack exchange,提问作者Tyler Kim
相关产品推荐
相关产品推荐

