Пользовательский клиент выборки, вызванный внутри Next.js getServerSideProps(), возвращает данные другого пользователя. Это утечка памяти? Если да, то где?

Вот реализация оболочки поверх API fetch, которую я использую в одном из проектов на работе. Эта оболочка fetch используется специально для извлечения данных из бэкэнда внутри getServerSideProps() страницы. Использование строго на стороне сервера, и хак используется для чтения токенов аутентификации через подписанный на стороне сервера cookie.

Мы получили жалобы от клиентов, которые видели на своей панели управления чужие данные, т. е. чужое имя и купленные товары. Однако обновление страницы решает эту проблему, и она не воспроизводится.

После повторного просмотра codeа на бэкэнде и безуспешных попыток найти что-либо даже после 3 дней копания в журналах я начал думать, что это может быть утечка памяти, которая, как известно, есть в старых версиях Next.js. Поэтому мы перешли на Next.js 14, и проблема исчезла.

Недавно у нас была ситуация, когда сервер перешел в режим нагрузки, и проблема снова возникла, пока нагрузка не снизилась. На этот раз даже мои товарищи по команде могли видеть проблему, и выполнение обновления каждый раз приносило случайные данные людей.

Мы перешли на клиентскую отрисовку для страниц, связанных с пользовательскими данными, что решило проблему. Но мне интересно, что именно вызывает здесь утечку памяти. Поскольку я потратил значительные усилия на то, чтобы утечки памяти не было никоим образом, насколько мне известно.

Есть ли вероятность того, что это произойдет в codeе ниже? Я открыт для любых других предложений, которые могут у вас возникнуть по поводу производительности/читабельности/чего угодно.

Вот как я это называю.


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 } });
}
Тамара
Вопрос задан27 июня 2024 г.

1 Ответ

2
Дорофей
Ответ получен16 сентября 2024 г.

Ваш ответ

Загрузить файл.