Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an express cache for graphql #1249

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions backend/middlewares/expressGraphqlCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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"

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<void> => {
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
7 changes: 6 additions & 1 deletion backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@
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,
Expand Down Expand Up @@ -54,8 +57,10 @@
) => {
const { prisma, logger, knex, extraContext } = serverContext
await createDefaultData(prisma)
// cache middleware first so that it's the first to run
app.use(createExpressGraphqlCacheMiddleware(logger))

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a database access
, but is not rate-limited.
app.use(
isProduction ? "/api" : "/",
GRAPHQL_ENDPOINT_PATH,
expressMiddleware(apolloServer, {
context: async (ctx) => ({
...ctx,
Expand Down