From 826de6d11c33a1d9b0c2e0bbb3b75363903dad91 Mon Sep 17 00:00:00 2001 From: Henrik Nygren Date: Thu, 12 Oct 2023 21:52:43 +0300 Subject: [PATCH 1/2] Add an express cache for graphql --- backend/middlewares/expressGraphqlCache.ts | 72 ++++++++++++++++++++++ backend/server.ts | 7 ++- 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 backend/middlewares/expressGraphqlCache.ts diff --git a/backend/middlewares/expressGraphqlCache.ts b/backend/middlewares/expressGraphqlCache.ts new file mode 100644 index 000000000..283f8e557 --- /dev/null +++ b/backend/middlewares/expressGraphqlCache.ts @@ -0,0 +1,72 @@ +import { createHash } from "crypto" + +import { NextFunction, Request, Response } from "express" +import { Logger } from "winston" + +import redisClient from "../services/redis" +import { GRAPHQL_ENDPOINT_PATH } from "/server" + +const CACHE_EXPIRE_TIME_SECONDS = 300 + +/** Express middleware, used for caching graphql queries before they hit the graphql server +Only used for queries and for requests that are not authenticated. */ +const createExpressGraphqlCacheMiddleware = (logger: Logger) => { + const expressCacheMiddleware = async ( + req: Request, + res: Response, + next: NextFunction, + ): Promise => { + if (!redisClient || !redisClient.isReady) { + return next() + } + // if user is logged in, return + if (req.headers.authorization !== undefined) { + logger.info(`Skipping express cache, authorization header is set`) + return next() + } + // Only handle graphql + if (req.path !== GRAPHQL_ENDPOINT_PATH || req.method !== "POST") { + return next() + } + + try { + const key = `express-graphql-response-cache-${createHash("sha512") + .update(JSON.stringify(req.body)) + .digest("hex")}` + const cachedResponseBody = await redisClient.get(key) + if (cachedResponseBody) { + logger.info( + `Express cache: Was able to find graphql response body from cache ${key}`, + ) + res.status(200) + res.setHeader("Content-Type", "application/json") + res.send(cachedResponseBody) + return + } + // Not found in cache, continue to graphql server but store the response in the cache + const originalSend = res.send + res.send = (body) => { + try { + if (redisClient && redisClient.isReady) { + logger.info( + `Express cache: Storing graphql response body to cache ${key}`, + ) + // Returns a promise but awaiting it here would be inconvenient and not necessary. It will get resolved eventually. + redisClient.set(key, body, { EX: CACHE_EXPIRE_TIME_SECONDS }) + } + } catch (e) { + logger.error(`Error when saving value to the express cache: ${e}`) + } + + return originalSend.call(res, body) + } + } catch (e) { + logger.error(`Error in express cache: ${e} `) + return next() + } + } + + return expressCacheMiddleware +} + +export default createExpressGraphqlCacheMiddleware diff --git a/backend/server.ts b/backend/server.ts index c2b44adc3..31bc46cdf 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -22,8 +22,11 @@ import { apiRouter } from "./api" import { DEBUG, isProduction, isTest } from "./config" import { createDefaultData } from "./config/defaultData" import { ServerContext } from "./context" +import createExpressGraphqlCacheMiddleware from "./middlewares/expressGraphqlCache" import { createSchema } from "./schema/common" +export const GRAPHQL_ENDPOINT_PATH = isProduction ? "/api" : "/" + // wrapped so that the context isn't cached between test instances const createExpressAppWithContext = ({ prisma, @@ -54,8 +57,10 @@ const addExpressMiddleware = async ( ) => { const { prisma, logger, knex, extraContext } = serverContext await createDefaultData(prisma) + // cache middleware first so that it's the first to run + app.use(createExpressGraphqlCacheMiddleware(logger)) app.use( - isProduction ? "/api" : "/", + GRAPHQL_ENDPOINT_PATH, expressMiddleware(apolloServer, { context: async (ctx) => ({ ...ctx, From 521b4b3bfb79b1908f12fe15a88a36e21a47b635 Mon Sep 17 00:00:00 2001 From: Henrik Nygren Date: Thu, 12 Oct 2023 22:17:01 +0300 Subject: [PATCH 2/2] Fix import --- backend/middlewares/expressGraphqlCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/middlewares/expressGraphqlCache.ts b/backend/middlewares/expressGraphqlCache.ts index 283f8e557..2de7a5df7 100644 --- a/backend/middlewares/expressGraphqlCache.ts +++ b/backend/middlewares/expressGraphqlCache.ts @@ -3,8 +3,8 @@ import { createHash } from "crypto" import { NextFunction, Request, Response } from "express" import { Logger } from "winston" +import { GRAPHQL_ENDPOINT_PATH } from "../server" import redisClient from "../services/redis" -import { GRAPHQL_ENDPOINT_PATH } from "/server" const CACHE_EXPIRE_TIME_SECONDS = 300