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的请求上下文完全由浏览器隔离,不存在服务端共享状态的问题。
代码隐患排查
你的封装代码存在以下潜在风险:
- 手动原型赋值的不规范实现:通过
Object.setPrototypeOf创建客户端实例,而非直接实例化FetchClient类,这种方式破坏了JS的类实例化规范,可能在某些异步场景下导致上下文绑定异常。 - 错误处理中的
this丢失:responseParser().catch(this.errorCatcher)中,errorCatcher的this指向可能在catch回调中丢失(Promise回调的执行上下文是全局),导致无法正确绑定实例上下文。 - 全局
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

