Next.js本地调用AI API触发ReadableStream类型错误求助
Next.js本地环境调用AI API触发ReadableStream类型错误的问题与解决
问题现象
在Next.js应用的本地开发和构建版本中调用OpenAI或ReplicateAI API时,持续触发500错误,错误信息为TypeError: ReadableStream: Expected <ref *1> ReadableStream。尽管报错,但API请求实际成功(ReplicateAI仪表盘可看到结果),且部署后的版本无此问题。将NextResponse替换为标准Response可解决问题,怀疑是Next.js和TypeScript最新版本的兼容性问题。
完整错误信息
[CONVERSATION_ERROR TypeError: ReadableStream: Expected <ref *1> ReadableStream { _state: 'readable', _reader: undefined, _storedError: undefined, _disturbed: false, _readableStreamController: ReadableByteStreamController { _controlledReadableByteStream: [Circular *1], _pullAgain: false, _pulling: false, _byobRequest: null, _queueTotalSize: 0, _queue: SimpleQueue { _cursor: 0, _size: 0, _front: [Object], _back: [Object] }, _closeRequested: false, _started: false, _strategyHWM: 0, _pullAlgorithm: [Function: pullAlgorithm], _cancelAlgorithm: [Function: cancelAlgorithm], _autoAllocateChunkSize: undefined, _pendingPullIntos: SimpleQueue { _cursor: 0, _size: 0, _front: [Object], _back: [Object] } } } to be an instance of ReadableStream. at webidl.errors.exception (node:internal/deps/undici/undici:1679:14) at Object.ReadableStream (node:internal/deps/undici/undici:1878:31) at webidl.converters.BodyInit (node:internal/deps/undici/undici:4688:34) at new Response (node:internal/deps/undici/undici:4396:36) at new NextResponse (webpack-internal:///(rsc)/./node_modules/next/dist/server/web/spec-extension/response.js:38:9) at NextResponse.json (webpack-internal:///(rsc)/./node_modules/next/dist/server/web/spec-extension/response.js:67:16) at POST (webpack-internal:///(rsc)/./app/api/conversation/route.ts:48:99) at async /Users/myName/Documents/Coding/myProject/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:6:61466 ✓ Compiled /(dashboard)/(routes)/music/page in 686ms (1838 modules) ✓ Compiled /api/music/route in 170ms (883 modules) [MUSIC_ERROR TypeError: ReadableStream: Expected <ref *1> ReadableStream { _state: 'readable', _reader: undefined, _storedError: undefined, _disturbed: false, _readableStreamController: ReadableByteStreamController { _controlledReadableByteStream: [Circular *1], _pullAgain: false, _pulling: false, _byobRequest: null, _queueTotalSize: 0, _queue: SimpleQueue { _cursor: 0, _size: 0, _front: [Object], _back: [Object] }, _closeRequested: false, _started: false, _strategyHWM: 0, _pullAlgorithm: [Function: pullAlgorithm], _cancelAlgorithm: [Function: cancelAlgorithm], _autoAllocateChunkSize: undefined, _pendingPullIntos: SimpleQueue { _cursor: 0, _size: 0, _front: [Object], _back: [Object] } } } to be an instance of ReadableStream. at webidl.errors.exception (node:internal/deps/undici/undici:1679:14) at Object.ReadableStream (node:internal/deps/undici/undici:1878:31) at webidl.converters.BodyInit (node:internal/deps/undici/undici:4688:34) at new Response (node:internal/deps/undici/undici:4396:36) at new NextResponse (webpack-internal:///(rsc)/./node_modules/next/dist/server/web/spec-extension/response.js:38:9) at NextResponse.json (webpack-internal:///(rsc)/./node_modules/next/dist/server/web/spec-extension/response.js:67:16) at POST (webpack-internal:///(rsc)/./app/api/music/route.ts:47:99) at async /Users/MyName/Documents/Coding/MyProject/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:6:61466
路由代码
import { auth } from '@clerk/nextjs'; import { NextResponse } from 'next/server'; import OpenAI from 'openai'; import { increaseApiLimit, checkApiLimit } from '@/lib/api-limit'; import { checkSubscription } from '@/lib/subscription'; const openai = new OpenAI({ apiKey: process.env.OPEN_API_KEY, }); export async function POST(req: Request) { try { const { userId } = auth(); const body = await req.json(); const { messages } = body; if (!userId) { return new NextResponse("Unauthorized", { status: 401 }) } if(!openai.apiKey) { return new NextResponse("openai API Key not configured") } if (!messages) { return new NextResponse("Messages are required", { status: 400 }) } const freeTrial = await checkApiLimit(); const isPro = await checkSubscription(); if (!freeTrial && !isPro) { return new NextResponse("Free trial has expired.", { status: 403 }) } const response = await openai.chat.completions.create({ model: "gpt-3.5-turbo", messages }) if (!isPro) await increaseApiLimit(); return NextResponse.json(response.choices[0].message) } catch (error) { console.log('[CONVERSATION_ERROR', error); return new NextResponse('Internal error', { status: 500 }); } }
请求表单代码
'use client'; import axios from 'axios'; import { toast } from 'react-hot-toast'; import { Empty } from '@/components/Empty'; import { Loader } from '@/components/Loader'; import Heading from '@/components/Heading'; import { MessageSquare } from 'lucide-react'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { formSchema } from './constants'; import { Form, FormControl, FormField, FormItem } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; import { cn } from '@/lib/utils'; import { UserAvatar } from '@/components/UserAvatar'; import { BotAvatar } from '@/components/BotAvatar'; import useProModal from '@/hooks/useProModal'; const ConversationPage = () => { const [messages, setMessages] = useState<ChatCompletionMessageParam[]>([]); const router = useRouter(); const proModal = useProModal(); const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { prompt: '', }, }); const isResponseLoading = form.formState.isSubmitting; const onSubmit = async (values: z.infer<typeof formSchema>) => { try { const userMessage: ChatCompletionMessageParam = { role: 'user', content: values.prompt, }; const newMessages = [...messages, userMessage]; const response = await axios.post('/api/conversation', { messages: newMessages, }); setMessages((current) => [...current, userMessage, response.data]); form.reset(); } catch (error: any) { if (error?.response?.status === 403) { proModal.onOpen(); } else { toast.error("Something went wrong"); } } finally { router.refresh(); } }; return ( <div> <Heading title='Conversation' description='Our most advanced conversation model.' Icon={MessageSquare} iconColor='text-violet-500' bgColor='bg-violet-500/10' /> <div className='px-4 lg:px-8'> <div> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className=' rounded-lg border w-full p-4 px-3 md:px-6 focus-within:shadow-sm grid grid-cols-12 gap-2 ' > <FormField name='prompt' render={({ field }) => ( <FormItem className='col-span-12 lg:col-span-10'> <FormControl className='m-0 p-0'> <Input className='border-0 outline-none focus-visible:ring-0 focus-visible:ring-transparent' disabled={isResponseLoading} placeholder='How do I calculate the radius of a circle?' {...field} /> </FormControl> </FormItem> )} /> <Button className='col-span-12 lg:col-span-2 w-full' disabled={isResponseLoading} > Generate </Button> </form> </Form> </div> <div className='space-y-4 mt-4'> {isResponseLoading && ( <div className='p-8 rounded-lg w-full flex items-center justify-center bg-muted'> <Loader /> </div> )} {messages.length === 0 && !isResponseLoading && ( <Empty label={'No Conversation Started'} /> )} <div className='flex flex-col-reverse gap-y-4'> {messages.map((message, idx) => ( <div key={`${message.content}-${idx}`} className={cn( 'p-8 w-full flex items-start gap-x-8 rounded-lg', message.role === 'user' ? 'bg-white border border-black/10' : 'bg-muted' )} > {message.role === 'user' ? <UserAvatar /> : <BotAvatar />} <p className='text-sm'>{message.content}</p> </div> ))} </div> </div> </div> </div> ); }; export default ConversationPage;
问题原因
这是Next.js内部基于undici库实现的NextResponse与Node.js原生ReadableStream存在实例校验不兼容的问题:
NextResponse继承自undici的Response类,该类对传入的BodyInit进行严格类型校验,要求ReadableStream必须是undici实现的实例- OpenAI/ReplicateAI返回的响应数据中,可能携带Node.js原生的ReadableStream对象,导致undici的类型校验失败
- 部署环境(如Vercel)使用Edge Runtime,其运行时的ReadableStream实现与undici兼容,因此不会触发错误
解决方案
1. 临时替代方案
直接使用标准Web API的Response替换NextResponse,标准Response对不同环境的ReadableStream兼容性处理更宽松:
// 替换原返回语句 return Response.json(response.choices[0].message)
2. 修复数据序列化问题
手动将AI返回的对象转为纯JSON格式,避免携带ReadableStream等特殊对象:
return NextResponse.json(JSON.parse(JSON.stringify(response.choices[0].message)))
3. 版本回退
如果是最新版Next.js的兼容性问题,回退到稳定版本(如13.4.x系列),避免使用latest标签安装:
npm install next@13.4.20
内容的提问来源于stack exchange,提问作者MrCodewide
相关产品推荐
相关产品推荐

