Next.js 13.4.19调用OpenAI API触发headers未定义TypeError
Next.js 13.4.19 集成OpenAI API时的"Cannot read properties of undefined (reading 'headers')"错误排查
问题场景
使用Next.js 13.4.19集成OpenAI API,API路由结构配置正常,但向http://localhost:3000/api/course/createChapters发送POST请求时触发错误。请求参数:
{ "title":"calculus", "units":["algebra","differentiation","integration"] }
错误信息
- error TypeError: Cannot read properties of undefined (reading 'headers') at eval (webpack-internal:///(rsc)/./node_modules/.pnpm/next@13.4.19_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/server/future/route-modules/app-route/module.js:266:61) at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
相关代码文件
gpt.ts
import OpenAI from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); interface OutputFormat { [key: string]: string | string[] | OutputFormat; } export async function strict_output( system_prompt: string, user_prompt: string | string[], output_format: OutputFormat, default_category: string = "", output_value_only: boolean = false, model: string = "gpt-3.5-turbo", temperature: number = 1, num_tries: number = 3, verbose: boolean = false ) { const list_input: boolean = Array.isArray(user_prompt); const dynamic_elements: boolean = /<.*?>/.test(JSON.stringify(output_format)); const list_output: boolean = /\[.*?\]/.test(JSON.stringify(output_format)); let error_msg: string = ""; for (let i = 0; i < num_tries; i++) { let output_format_prompt: string = `\nYou are to output ${ list_output && "an array of objects in" } the following in json format: ${JSON.stringify( output_format )}. \nDo not put quotation marks or escape character \\ in the output fields.`; if (list_output) { output_format_prompt += `\nIf output field is a list, classify output into the best element of the list.`; } if (dynamic_elements) { output_format_prompt += `\nAny text enclosed by < and > indicates you must generate content to replace it. Example input: Go to <location>, Example output: Go to the garden\nAny output key containing < and > indicates you must generate the key name to replace it. Example input: {'<location>': 'description of location'}, Example output: {school: a place for education}`; } if (list_input) { output_format_prompt += `\nGenerate an array of json, one json for each input element.`; } const response = await openai.chat.completions.create({ temperature: temperature, model: model, messages: [ { role: "system", content: system_prompt + output_format_prompt + error_msg, }, { role: "user", content: user_prompt.toString() }, ], }); let res: string = response.choices[0].message?.content?.replace(/'/g, '"') ?? ""; res = res.replace(/(\w)"(\w)/g, "$1'$2"); if (verbose) { console.log( "System prompt:", system_prompt + output_format_prompt + error_msg ); console.log("\nUser prompt:", user_prompt); console.log("\nGPT response:", res); } try { let output: any = JSON.parse(res); if (list_input) { if (!Array.isArray(output)) { throw new Error("Output format not in an array of json"); } } else { output = [output]; } for (let index = 0; index < output.length; index++) { for (const key in output_format) { if (/<.*?>/.test(key)) { continue; } if (!(key in output[index])) { throw new Error(`${key} not in json output`); } if (Array.isArray(output_format[key])) { const choices = output_format[key] as string[]; if (Array.isArray(output[index][key])) { output[index][key] = output[index][key][0]; } if (!choices.includes(output[index][key]) && default_category) { output[index][key] = default_category; } if (output[index][key].includes(":")) { output[index][key] = output[index][key].split(":")[0]; } } } if (output_value_only) { output[index] = Object.values(output[index]); if (output[index].length === 1) { output[index] = output[index][0]; } } } return list_input ? output : output[0]; } catch (e) { error_msg = `\n\nResult: ${res}\n\nError message: ${e}`; console.log("An exception occurred:", e); console.log("Current invalid json format ", res); } } return []; }
route.ts
import { strict_output } from "@/lib/gpt"; import { createChaptersSchema } from "@/validators/course"; import { NextResponse } from "next/server"; import { ZodError } from "zod"; export const POST = async (req: Request) => { try { const body = await req.json(); const { title, units } = createChaptersSchema.parse(body); type outptUnits = { title: string; chapters: { youtube_search_query: string; chapter_title: string; }[]; }[]; let output_units: outptUnits = await strict_output( "You are an AI capable of curating course content, coming up with relevant chapter titles, and finding relevant youtube videos for each chapter", new Array(units.length).fill( `It is your job to create a course about ${title}. The user has requested to create chapters for each of the units. Then, for each chapter, provide a detailed youtube search query that can be used to find an informative educationalvideo for each chapter. Each query should give an educational informative course in youtube.` ), { title: "title of the unit", chapters: "an array of chapters, each chapter should have a youtube_search_query and a chapter_title key in the JSON object", } ); console.log(output_units); return NextResponse.json(output_units); } catch (error) { if (error instanceof ZodError) { return new NextResponse("Invalid Body", { status: 400 }); } console.error("API Error:", error); return new NextResponse("Internal Server Error", { status: 500 }); } };
Prisma Schema
model Course { id String @id @default(cuid()) name String image String units Unit[] } model Unit { id String @id @default(cuid()) name String chapters Chapter[] courseId String course Course @relation(fields: [courseId], references: [id]) @@index([courseId], name:"courseId") } model Chapter { id String @id @default(cuid()) unitId String name String youtubeSearchQuery String videoId String? summary String? @db.VarChar(3000) unit Unit @relation(fields: [unitId], references: [id]) questions Question[] @@index([unitId], name:"unitId") } model Question { id String @id @default(cuid()) chapterId String questionId String chapter Chapter @relation(fields: [chapterId], references: [id]) question String @db.VarChar(3000) options String @db.VarChar(3000) answer String @db.VarChar(3000) @@index([chapterId], name:"chapterId") }
文件夹结构

预期结果
使用Insomnia发送请求后,期望得到包含章节标题和YouTube搜索词的JSON结果:
错误原因及修复方案
核心问题
- 错误处理不完整:原
route.ts仅捕获ZodError,其他错误未处理,导致函数返回undefined,Next.js读取响应headers时触发报错。 - 类型不匹配:
strict_output接收数组类型的user_prompt,返回数组,但原代码中outptUnits定义为单个对象类型,导致类型冲突。 - 请求参数格式错误:原请求使用单引号,不符合JSON标准。
修复步骤
- 完善错误处理:在
route.ts的catch块中添加默认错误返回,确保始终返回NextResponse。 - 修正类型定义:将
outptUnits改为数组类型,匹配strict_output的返回值。 - 规范请求参数:将请求JSON中的单引号改为双引号。
- 验证OpenAI配置:确保
OPENAI_API_KEY环境变量已正确配置。
内容的提问来源于stack exchange,提问作者pradip subedi
相关产品推荐
相关产品推荐

