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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 09:52:03