Вот реализация оболочки поверх 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 } });
}