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

Next.js getServerSideProps中自定义FetchClient返回他人数据,是否为内存泄漏?

问题背景与排查经过

我在项目中实现了一个基于fetch API的封装客户端,专门用于Next.js的getServerSideProps()方法中从后端获取数据,仅在服务端运行,通过服务端签名Cookie读取认证令牌。

近期收到用户反馈:仪表盘会显示其他用户的姓名、已购商品等信息,刷新页面可临时解决,但问题无法稳定复现。排查后端代码及日志3天无结果后,推测是旧版Next.js的内存泄漏问题,升级到Next.js 14后问题消失。但近期服务器高负载时,问题再次出现,刷新页面会随机显示不同用户的数据。切换为客户端渲染(CSR)后问题彻底解决,但我想明确:

  • 此问题是否由内存泄漏导致?
  • 下方的封装代码是否存在相关隐患?
  • 欢迎提供代码在性能、可读性等方面的优化建议。
代码实现

封装客户端代码

import { ACCESS_TOKEN } from "constants/cookie"; // cookie names for rotating
import { getCookie } from "cookies-next";

export function unstringify(value) {
  try {
    return JSON.parse(value);
  } catch (error) {
    return value;
  }
}

export function loadFromCookies(key, options) {
  return unstringify(getCookie(key, options) ?? null);
}

import { createLogger } from "logger/debug";
const debug = createLogger("fetchClient");
const verbose = debug.extend("verbose");

const isServer = typeof window === "undefined";

class FetchClient {
  constructor(defaultConfig) {
    this.defaultConfig = defaultConfig ?? {};
  }

  async request(method = "GET", endpoint, body, options) {
    const { baseURL, parseResponse, ...fetchOptions } = {
      ...this.defaultConfig,
      ...options,
      headers: {
        ...this.defaultConfig?.headers,
        ...options?.headers,
      },
      method,
    };

    if (body && !["HEAD", "GET", "DELETE"].includes(method)) {
      fetchOptions.body = typeof body === "string" ? body : JSON.stringify(body);
    }

    const target = (baseURL ?? "") + endpoint;
    debug("Q-> %s %s", method, target);
    verbose("Q-> %s %s %O", method, target, fetchOptions);

    const response = await fetch(target, fetchOptions);
    verbose("<-S %s %s %O", method, target, response.headers);

    if (!response.ok) console.error(`(${response.status}) ${response.statusText} | ${method} ${target}`);
    return this.responseParser({ response, parseResponse }).catch(this.errorCatcher);
  }

  async responseParser({ response, parseResponse }) {
    if (response.status === 204) return;
    if (parseResponse === false) return response;

    const contentType = response.headers.has("content-type") && response.headers.get("content-type");
    debug("<-S content-type %o", contentType);
    if (!contentType) return response;

    if (contentType.includes("application/json")) {
      const body = await response.json();
      verbose("<-S json %O", body);
      return body;
    }
  }

  head(endpoint, options) {
    return this.request("HEAD", endpoint, null, options);
  }

  get(endpoint, options) {
    return this.request("GET", endpoint, null, options);
  }

  delete(endpoint, options) {
    return this.request("DELETE", endpoint, null, options);
  }

  post(endpoint, body, options) {
    return this.request("POST", endpoint, body, options);
  }

  put(endpoint, body, options) {
    return this.request("PUT", endpoint, body, options);
  }

  patch(endpoint, body, options) {
    return this.request("PATCH", endpoint, body, options);
  }

  errorCatcher(error) {
    console.error(error);
    return {};
  }
}

const defaults = Object.freeze({
  headers: { "Content-Type": "application/json" },
});

const fetchClient = new FetchClient(defaults);
const fetchClientPrototype = Object.getPrototypeOf(fetchClient);

function SSR({ req, res }) {
  const context = Object.assign({}, this.defaultConfig);

  const token = loadFromCookies(ACCESS_TOKEN.KEY, { req, res }) ?? loadFromCookies(ACCESS_TOKEN.OLD_KEY, { req, res });
  if (token) {
    debug("fetchSSR token found", { isServer });
    context.headers = Object.assign(context.headers ?? {}, { Authorization: `Bearer ${token}` });
  }

  return Object.setPrototypeOf({ defaultConfig: context }, fetchClientPrototype);
}

const fetchSSR = SSR.bind(fetchClient);

export default fetchSSR;

调用方式

export async function getServerSideProps(ctx) {
  const user = findUserFromRequest(ctx);
  const fetchClient = fetchSSR(ctx);

  const { data: purchasedItems = [] } = user ? await fetchClient.get("/path/to/purchased") : {};

  return withServerProps({ ctx, fetchClient, props: { purchasedItems } });
}
核心分析与结论

问题根源判断

该问题大概率是服务端请求上下文泄漏导致的,而非传统意义上的内存泄漏。Next.js的getServerSideProps在Node.js单线程事件循环中处理请求,高负载下如果存在未正确隔离的共享状态,就会出现请求之间的上下文污染(比如A请求的认证令牌被B请求复用)。

升级Next.js 14后问题消失,是因为新版本对服务端渲染的请求隔离做了优化,但高负载下极端场景的边界case仍可能触发。切换CSR后问题解决,是因为CSR的请求上下文完全由浏览器隔离,不存在服务端共享状态的问题。

代码隐患排查

你的封装代码存在以下潜在风险:

  1. 手动原型赋值的不规范实现:通过Object.setPrototypeOf创建客户端实例,而非直接实例化FetchClient类,这种方式破坏了JS的类实例化规范,可能在某些异步场景下导致上下文绑定异常。
  2. 错误处理中的this丢失:responseParser().catch(this.errorCatcher)中,errorCatcher的this指向可能在catch回调中丢失(Promise回调的执行上下文是全局),导致无法正确绑定实例上下文。
  3. 全局fetchClient实例的存在:虽然SSR函数每次都会创建新的context,但全局实例的存在仍可能引入不必要的共享状态风险,尤其是在Next.js的服务端运行时环境中。
优化建议

1. 改用标准类实例化方式

替换手动原型赋值的逻辑,直接在SSR函数中创建新的FetchClient实例,更清晰且符合规范:

// 移除全局fetchClient和原型相关代码
function fetchSSR({ req, res }) {
  const context = Object.assign({}, defaults);
  const token = loadFromCookies(ACCESS_TOKEN.KEY, { req, res }) ?? loadFromCookies(ACCESS_TOKEN.OLD_KEY, { req, res });
  if (token) {
    debug("fetchSSR token found", { isServer });
    context.headers = Object.assign(context.headers ?? {}, { Authorization: `Bearer ${token}` });
  }
  return new FetchClient(context);
}

export default fetchSSR;

2. 修复错误处理的this绑定

在类的构造函数中绑定errorCatcher的上下文,或者使用箭头函数:

class FetchClient {
  constructor(defaultConfig) {
    this.defaultConfig = defaultConfig ?? {};
    // 绑定上下文
    this.errorCatcher = this.errorCatcher.bind(this);
  }

  // 或者直接用箭头函数
  errorCatcher = (error) => {
    console.error(error);
    return {};
  }
}

3. 增强错误处理逻辑

不要在客户端内部吞噬错误,应该将错误抛出,让调用端(getServerSideProps)处理,避免因为错误导致返回空对象而引发业务逻辑异常:

errorCatcher = (error) => {
  console.error(error);
  // 抛出错误,让调用端感知
  throw error;
}

4. 优化响应解析逻辑

补充非JSON响应的处理,避免遗漏场景:

async responseParser({ response, parseResponse }) {
  if (response.status === 204) return null;
  if (parseResponse === false) return response;

  const contentType = response.headers.get("content-type");
  debug("<-S content-type %o", contentType);
  
  if (!contentType) return await response.text();

  if (contentType.includes("application/json")) {
    const body = await response.json();
    verbose("<-S json %O", body);
    return body;
  }
  
  // 处理其他类型的响应
  return await response.text();
}

5. 移除不必要的全局变量

isServer变量在SSR函数中并未使用,可以移除,或者仅在需要的地方判断。

6. 增加类型定义(可选,若使用TypeScript)

为FetchClient和相关函数添加类型注解,提升代码可读性和可维护性。

内容的提问来源于stack exchange,提问作者Akash Patel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 12:10:57