Next.js生产主域名下服务端generateMetadata中Fetch请求超时,其他环境/子域名正常
Next.js生产主域名下服务端generateMetadata中Fetch请求超时,其他环境/子域名正常
根据你的描述,Next.js在generateMetadata函数中发起的服务端fetch请求,在本地、测试HTTPS环境和子域名都正常,但主域名生产环境出现UND_ERR_CONNECT_TIMEOUT连接超时错误,且客户端请求完全正常。结合你的代码和日志,我整理了几个核心排查方向和解决方案:
1. 优先排查生产服务器的内网连通性
服务端的fetch请求是从Next.js部署的服务器发起的,而非客户端浏览器,所以最可能的原因是生产服务器无法访问后端API的网络链路问题:
- 登录Next.js生产服务器,执行curl命令模拟服务端请求:
curl -v -H "Accept-Language: en" https://new..io/api/blog/team-performance-metrics-what-to-measure-and-why - 如果curl也超时,说明是纯网络问题:
- 检查DNS解析:执行
nslookup new..io,看是否能正常解析到后端服务器的IP - 检查防火墙/安全组:确认Next.js服务器的出站规则允许访问后端的443端口,同时后端服务器的入站规则允许Next.js服务器的IP访问
- 若后端和Next.js服务器在同一内网,尝试用内网IP直接访问API(比如
http://192.168.xxx.xxx/api/...),排除公网DNS或路由问题
- 检查DNS解析:执行
2. 简化Fetch请求的自定义Headers
你当前手动设置了Origin、Referer、Accept-Encoding等Headers,但这些是客户端浏览器专属的请求头,服务端发起请求时添加这些可能触发后端的安全拦截,甚至导致异常:
- 建议简化Headers配置,只保留必要的字段:
headers: { Accept: 'application/json', // 只保留JSON接受头 'Accept-Language': locale || i18nConfig.defaultLocale, } - 移除
Origin、Referer、Accept-Encoding:Next.js的fetch会自动处理编码,服务端请求不需要Origin/Referer
3. 调整超时时间配置
你当前设置的超时时间只有1秒(const timeout = 1_000),生产环境网络可能存在延迟,这个时间过短会导致不必要的超时终止:
- 调整超时时间为10秒甚至更长,测试是否还会超时:
const timeout = 10_000; // 10秒超时 const timeoutId = setTimeout(() => { controller.abort() logToFile('❌ Request timed out') }, timeout)
4. 检查后端Yii2的安全验证规则
如果网络连通性正常,可能是后端Yii2的安全配置拦截了主域名的服务端请求:
- 检查Yii2的
AccessControl组件:是否设置了IP白名单,未包含Next.js生产服务器的IP - 检查CSRF验证:虽然服务端请求不需要CSRF Token,但如果后端强制开启了全局CSRF验证,可能会拒绝无Token的请求(此时一般返回403而非超时,但仍需排查)
- 查看后端Nginx/Apache的访问日志,确认是否收到了Next.js生产服务器的请求,以及返回的状态码
5. 修正Fetch的配置细节
结合你的代码,还有几个小细节可以优化:
- 确认生产环境的
NEXT_PUBLIC_BASE_URL环境变量是否正确(比如末尾是否多了斜杠,导致API URL拼接错误),可以在日志中打印完整的环境变量值:logToFile(`Base URL from env: ${process.env.NEXT_PUBLIC_BASE_URL}`) - 若排查期间可以暂时关闭
next.revalidate缓存,直接请求后端,排除缓存逻辑的干扰
优化后的示例代码
export async function generateMetadata({ params, }: { params: Promise<{ slug: string, locale: string }> }): Promise<Metadata> { console.log('metadata start') const { slug, locale } = await params const apiUrl = `${process.env.NEXT_PUBLIC_BASE_URL}${BLOG_DETAIL.replace('{slug}', slug)}` logToFile(` Fetching metadata for slug: ${slug}`) logToFile(` API URL: ${apiUrl}`) logToFile(` Locale: ${locale}`) logToFile(`Base URL from env: ${process.env.NEXT_PUBLIC_BASE_URL}`) const controller = new AbortController() const timeout = 10_000; // 调整为10秒超时 const timeoutId = setTimeout(() => { controller.abort() logToFile('❌ Request timed out') }, timeout) try { const res = await fetch(apiUrl, { signal: controller.signal, next: { revalidate: 600 }, headers: { Accept: 'application/json', 'Accept-Language': locale || i18nConfig.defaultLocale, }, }) logToFile(`✅ Response status: ${res.status}`) if (!res.ok) { logToFile(`❌ Non-OK response: ${res.status}`) throw new Error(`API returned status ${res.status}`) } const data = await res.json() logToFile(`✅ Parsed metadata: ${JSON.stringify(data)}`) const { title, subtitle: description, photo } = data.data return { title, description, openGraph: { title, description, images: [photo.thumbnail], }, twitter: { title, description, images: [photo.thumbnail], }, } } catch (error: any) { clearTimeout(timeoutId) logToFile(`❌ Fetch error: ${error?.message}`) if (error?.cause) logToFile(`Cause: ${JSON.stringify(error.cause)}`) return { title: 'Ошибка загрузки блога', description: 'Не удалось получить данные этого блога', } } }
内容来源于stack exchange
相关产品推荐
相关产品推荐

