Here is the implementation of a wrapper on top of fetch API that I use in one of the projects at work. This fetch wrapper is used specifically to fetch data from backend inside the getServerSideProps() of the page. The usage is strictly server-side and a hack is used to read authentication tokens via server-side signed cookie.
We have received complaints from customers seeing someone else’s details on their dashboard i.e. Someone else’s name and purchased items. However, a refresh of the page solves it and it is not reproducible.
After repeated review of the code on backend and failing to find anything even after 3 days of logs digging, I started believing that this might be a memory leak Next.js is known to have in older versions. So we switched to Next.js 14 and the issue went away.
Recently we had a situation where the server went into stress and the issue resurfaced till the load subsided. This time even my teammates could see the issue and performing a refresh brought random people’s data each time.
We shifted to client-side rendering for userdata related pages, which solved the issue. But I am curious what is exactly introducing the memory leak here. Since I have spent considerable effort in ensuring there should not be a memory leak by any means, to the best of my knowledge.
Is there any possibility of this happening in the code below?
I’m open to any other suggestion you might have about performance/readability/anything.
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;
Here’s how i call it.
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 } });
}