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

NextJS 13.5.6 API路由缓存异常问题求助

NextJS 13.5.6 API路由缓存异常问题

项目流程

  • 浏览器调用NextJS内部API
  • NextJS API转发请求至外部微服务API

问题现象

  • 修改微服务数据后,NextJS API仍返回旧缓存数据
  • 排除浏览器缓存:更换浏览器、使用隐身模式测试,问题依旧
  • 排除外部API缓存:API网关日志显示外部API返回最新数据

已尝试的修复措施

  • 所有fetch请求均添加{ cache: 'no-store' }配置
  • 未启用Redis或其他第三方缓存机制

项目结构

apiBuilder
├── nextJSApiBuilder.ts
└── remoteApiBuilder.ts
app
├── api
│   └── config
│       └── route.ts
└── page.tsx
  • 页面通过nextJSApiBuilder.ts调用/api/config
  • /api/config/route.ts通过remoteApiBuilder.ts调用外部API
  • 所有页面均使用use client,未启用SSR

疑问点

  • 查阅NextJS数据缓存文档后仍无法定位原因
  • 看到建议在route.ts中添加export const revalidate = 0;,但疑惑:服务端运行的route.ts为何需要强制设为动态?

附:相关代码实现

nextJSApiBuilder.ts

const localApiBuilder = async (
  url: string,
  method: string,
  payload: any = undefined
) => {
  const headers = new Headers();
  try {
    const options: RequestInit = {
      method: method,
      headers: headers,
      cache: "no-store",
    };

    if (payload !== undefined) {
      options.body = JSON.stringify(payload);
    }

    const res = await fetch(
      process.env.NEXT_PUBLIC_INTERNAL_API_URL + url,
      options
    );
    const data = await res.json();

    if (!res.ok) {
      throw new Error(JSON.stringify(data));
    }

    return data;
  } catch (e: any) {
    throw e;
  }
};

export default localApiBuilder;

remoteApiBuilder.ts

import RemoteApiCustomError from "@/model/remoteApiResponse/RemoteApiCustomError";
import RemoteApiErrorType from "@/model/remoteApiResponse/RemoteApiErrorType";
import RemoteApiFailedResponse from "@/model/remoteApiResponse/RemoteApiFailedResponse";
import RemoteApiSuccessResponse from "@/model/remoteApiResponse/RemoteApiSuccessResponse";

const remoteApiBuilder = async <T>(url: string, body: T | undefined) => {
  const controller = new AbortController();
  const timoutId = setTimeout(() => controller.abort(), 20000);

  const headers = new Headers();
  headers.append("Content-Type", "application/json");
  headers.append(
    "Apim-Subscription-Key",
    process.env.NEXT_PUBLIC_API_KEY as string
  );

  const options: RequestInit = {
    method: body ? "post" : "get",
    headers: headers,
    signal: controller.signal,
    cache: "no-store",
  };

  if (body) {
    options.body = typeof body === "string" ? body : JSON.stringify(body);
  }

  try {
    const response = await fetch(
      process.env.NEXT_PUBLIC_BACKEND_URL + url,
      options
    );
    const data = await response.json();
    console.log(data);

    if (!response.ok) {
      const errorResponse = data as RemoteApiFailedResponse;
      console.log("response is not ok");
      console.log(errorResponse);
      throw new RemoteApiCustomError(
        RemoteApiErrorType.API_FAILED_ERROR,
        errorResponse.errorMessage
      );
    }

    const possiblyFailedResponse = data as RemoteApiSuccessResponse;
    if (possiblyFailedResponse.errorCode !== 0) {
      throw new RemoteApiCustomError(
        RemoteApiErrorType.NON_ZERO_ERROR,
        possiblyFailedResponse.errorMessage
      );
    }

    console.log(
      "fetch success response",
      possiblyFailedResponse.response,
      url,
      options
    );

    return possiblyFailedResponse.response;
  } catch (error: any) {
    if (error.name === "AbortError") {
      throw new RemoteApiCustomError(
        RemoteApiErrorType.TIME_OUT_ERROR,
        "Request timed out"
      );
    } else if (error instanceof RemoteApiCustomError) {
      throw error;
    } else {
      throw new RemoteApiCustomError(
        RemoteApiErrorType.UNKNOWN_ERROR,
        JSON.stringify(error)
      );
    }
  } finally {
    clearTimeout(timoutId);
  }
};

export default remoteApiBuilder;

api/config/route.ts

import FNPConfig from "@/model/config/FNPConfig";
import { NextRequest, NextResponse } from "next/server";
import remoteApiBuilder from "@/apiBuilder/remote.api.builder/remote.api.builder";
import translateRemoteApiCustomError from "@/utils/translateRemoteApiCustomError/translateRemoteApiCustomError";
import performRequestBasicCheck from "@/utils/performRequestBasicCheck/performRequestBasicCheck";

export async function GET(request: NextRequest) {
  performRequestBasicCheck(request, request.method, "GET");

  try {
    const response: FNPConfig[] = (await remoteApiBuilder(
      "/RetrieveFNPConfig",
      undefined
    )) as FNPConfig[];

    return NextResponse.json(response ? response[0] : [], { status: 200 });
  } catch (e: any) {
    return translateRemoteApiCustomError(e);
  }
}

页面调用函数

import { GET } from "@/constants/config";
import FNPConfig from "@/model/config/FNPConfig";
import localApiQueryBuilder from "@/apiBuilder/local.api.builder.ts/local.api.builder";

const localConfigApi = async (): Promise<FNPConfig> => {
  try {
    const response: FNPConfig = (await localApiQueryBuilder(
      "/config",
      GET
    )) as FNPConfig;
    return response ? response : new FNPConfig();
  } catch (e: any) {
    throw e;
  }
};

export default localConfigApi;

问题根源与解决方案

根源分析

在NextJS App Router中,API路由默认采用静态渲染缓存:即使你在fetch中添加了cache: 'no-store',如果API路由本身没有动态依赖(如request的查询参数、headers、cookies等),NextJS会默认将其标记为静态路由,缓存首次请求或构建时的响应结果。

你的/api/config/route.ts虽然接收了NextRequest参数,但并未使用任何动态属性,因此NextJS判定其为静态路由,直接返回缓存数据,不会重新执行内部的remoteApiBuilder调用。

解决方案

方案1:强制路由动态化(推荐)

在api/config/route.ts顶部添加以下代码,强制禁用路由缓存:

export const revalidate = 0; // 标记为动态路由,每次请求都重新执行

方案2:添加动态依赖触发动态路由

如果不想全局禁用缓存,可以在GET方法中读取任意动态属性(即使不用),触发NextJS将路由标记为动态:

export async function GET(request: NextRequest) {
  // 读取headers触发动态路由判定
  request.headers.get('user-agent');
  performRequestBasicCheck(request, request.method, "GET");
  // ... 剩余逻辑不变
}

方案3:设置响应缓存控制头

在返回响应时显式添加Cache-Control头,明确禁止缓存:

return NextResponse.json(response ? response[0] : [], { 
  status: 200,
  headers: {
    'Cache-Control': 'no-store, max-age=0'
  }
});

验证步骤

  1. 应用上述任意方案后,重启开发服务器(生产环境需重新构建)
  2. 修改微服务数据,调用NextJS API验证是否返回最新结果
  3. 查看服务端日志,确认每次请求都会执行remoteApiBuilder中的console.log逻辑

内容的提问来源于stack exchange,提问作者Kyaw Soe Hein

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 17:24:52