Skip to content

Commit 9320314

Browse files
committed
commented out the critical aspecs of the middleware execution
1 parent 61bcfd4 commit 9320314

File tree

3 files changed

+42
-32
lines changed

3 files changed

+42
-32
lines changed

src/middleware/index.ts

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ import ASTParser from '../analysis/ASTParser';
1313
* Primary entry point for adding GraphQL Rate Limiting middleware to an Express Server
1414
* @param {GraphQLSchema} schema GraphQLSchema object
1515
* @param {ExpressMiddlewareConfig} middlewareConfig
16-
* /// "ratelimiter" must be explicitly specified in the setup of the middleware.
17-
* /// "redis" connection options (https://ioredis.readthedocs.io/en/stable/API/#new_Redis) and an optional "keyExpiry" property (defaults to 24h)
18-
* /// "typeWeights" optional type weight configuration for the GraphQL Schema. Developers can override default typeWeights. Defaults to {mutation: 10, query: 1, object: 1, scalar/enum: 0, connection: 2}
19-
* /// "dark: true" will run the package in "dark mode" to monitor queries and rate limiting data before implementing rate limitng functionality. Defaults to false
20-
* /// "enforceBoundedLists: true" will throw an error if any lists in the schema are not constrained by slicing arguments: Defaults to false
21-
* /// "depthLimit: number" will block queries with deeper nesting than the specified depth. Will not block queries by depth by default
16+
* , "ratelimiter" must be explicitly specified in the setup of the middleware.
17+
* , "redis" connection options (https://ioredis.readthedocs.io/en/stable/API/#new_Redis) and an optional "keyExpiry" property (defaults to 24h)
18+
* , "typeWeights" optional type weight configuration for the GraphQL Schema. Developers can override default typeWeights. Defaults to {mutation: 10, query: 1, object: 1, scalar/enum: 0, connection: 2}
19+
* , "dark: true" will run the package in "dark mode" to monitor queries and rate limiting data before implementing rate limitng functionality. Defaults to false
20+
* , "enforceBoundedLists: true" will throw an error if any lists in the schema are not constrained by slicing arguments: Defaults to false
21+
* , "depthLimit: number" will block queries with deeper nesting than the specified depth. Will not block queries by depth by default
2222
* @returns {RequestHandler} express middleware that computes the complexity of req.query and calls the next middleware
2323
* if the query is allowed or sends a 429 status if the request is blocked
2424
* FIXME: How about the specific GraphQLError?
25-
* @throws ValidationError if GraphQL Schema is invalid.
25+
* @throws Error
2626
*/
2727
export default function expressGraphQLRateLimiter(
2828
schema: GraphQLSchema,
@@ -46,34 +46,38 @@ export default function expressGraphQLRateLimiter(
4646
enforceBoundedLists: middlewareConfig.enforceBoundedLists || false,
4747
depthLimit: middlewareConfig.depthLimit || Infinity,
4848
};
49-
if (middlewareSetup.depthLimit <= 1) {
49+
50+
/** No query can have a depth of less than 2 */
51+
if (middlewareSetup.depthLimit <= 2) {
5052
throw new Error(
5153
`Error in expressGraphQLRateLimiter: depthLimit cannot be less than or equal to 1`
5254
);
5355
}
54-
/**
55-
* build the type weight object, create the redis client and instantiate the ratelimiter
56-
* before returning the express middleware that calculates query complexity and throttles the requests
57-
*/
56+
57+
/** Build the type weight object, create the redis client and instantiate the ratelimiter */
5858
const typeWeightObject = buildTypeWeightsFromSchema(
5959
schema,
6060
middlewareSetup.typeWeights,
6161
middlewareSetup.enforceBoundedLists
6262
);
63-
const redisClient = connect(middlewareSetup.redis.options); // Default port is 6379 automatically
63+
const redisClient = connect(middlewareSetup.redis.options);
6464
const rateLimiter = setupRateLimiter(
6565
middlewareSetup.rateLimiter,
6666
redisClient,
6767
middlewareSetup.redis.keyExpiry
6868
);
6969

70-
// stores request IDs to be processed
71-
const requestQueue: { [index: string]: string[] } = {};
72-
73-
// Manages processing of event queue
70+
/**
71+
* We are using a queue and event emitter to handle situations where a user has two concurrent requests being processed.
72+
* The trailing request will be added to the queue to and await the prior request processing by the rate-limiter
73+
* This will maintain the consistency and accuracy of the cache when under load from one user
74+
*/
75+
// stores request IDs for each user in an array to be processed
76+
const requestQueues: { [index: string]: string[] } = {};
77+
// Manages processing of requests queue
7478
const requestEvents = new EventEmitter();
7579

76-
// Resolves the promise created by throttledProcess
80+
// processes requests (by resolving promises) that have been throttled by throttledProcess
7781
async function processRequestResolver(
7882
userId: string,
7983
timestamp: number,
@@ -83,11 +87,11 @@ export default function expressGraphQLRateLimiter(
8387
) {
8488
try {
8589
const response = await rateLimiter.processRequest(userId, timestamp, tokens);
86-
requestQueue[userId] = requestQueue[userId].slice(1);
87-
// trigger the next event
90+
requestQueues[userId] = requestQueues[userId].slice(1);
8891
resolve(response);
89-
requestEvents.emit(requestQueue[userId][0]);
90-
if (requestQueue[userId].length === 0) delete requestQueue[userId];
92+
// trigger the next event and delete the request queue for this user if there are no more requests to process
93+
requestEvents.emit(requestQueues[userId][0]);
94+
if (requestQueues[userId].length === 0) delete requestQueues[userId];
9195
} catch (err) {
9296
reject(err);
9397
}
@@ -112,13 +116,13 @@ export default function expressGraphQLRateLimiter(
112116
// Alternatively use crypto.randomUUID() to generate a random uuid
113117
const requestId = `${timestamp}${tokens}`;
114118

115-
if (!requestQueue[userId]) {
116-
requestQueue[userId] = [];
119+
if (!requestQueues[userId]) {
120+
requestQueues[userId] = [];
117121
}
118-
requestQueue[userId].push(requestId);
122+
requestQueues[userId].push(requestId);
119123

120124
return new Promise((resolve, reject) => {
121-
if (requestQueue[userId].length > 1) {
125+
if (requestQueues[userId].length > 1) {
122126
requestEvents.once(requestId, async () => {
123127
await processRequestResolver(userId, timestamp, tokens, resolve, reject);
124128
});
@@ -128,6 +132,7 @@ export default function expressGraphQLRateLimiter(
128132
});
129133
}
130134

135+
/** Rate-limiting middleware */
131136
return async (
132137
req: Request,
133138
res: Response,
@@ -145,9 +150,9 @@ export default function expressGraphQLRateLimiter(
145150
const ip: string = req.ips ? req.ips[0] : req.ip;
146151

147152
const queryAST = parse(query);
148-
// validate the query against the schema. The GraphQL validation function returns an array of errors.
153+
// validate the query against the schema. returns an array of errors.
149154
const validationErrors = validate(schema, queryAST);
150-
// check if the length of the returned GraphQL Errors array is greater than zero. If it is, there were errors. Call next so that the GraphQL server can handle those.
155+
// return the errors to the client if the array has length. otherwise there are no errors
151156
if (validationErrors.length > 0) {
152157
res.status(400).json({ errors: validationErrors });
153158
}
@@ -156,8 +161,6 @@ export default function expressGraphQLRateLimiter(
156161
const queryComplexity = queryParser.processQuery(queryAST);
157162

158163
try {
159-
// process the request and conditinoally respond to client with status code 429 or
160-
// pass the request onto the next middleware function
161164
const rateLimiterResponse = await throttledProcess(
162165
ip,
163166
requestTimestamp,
@@ -172,11 +175,17 @@ export default function expressGraphQLRateLimiter(
172175
queryParser.maxDepth >= middlewareSetup.depthLimit,
173176
depth: queryParser.maxDepth,
174177
};
178+
/** The three conditions for returning a status code 429 are
179+
* 1. rate-limiter blocked the request
180+
* 2. query exceeded the depth limit
181+
* 3. the middleware is configured not to run in dark mode
182+
*/
175183
if (
176184
(!rateLimiterResponse.success ||
177185
queryParser.maxDepth > middlewareSetup.depthLimit) &&
178186
!middlewareSetup.dark
179187
) {
188+
// a Retry-After header of Infinity means the request will never be accepted
180189
return res
181190
.status(429)
182191
.set({

src/utils/redis.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function connect(options: RedisOptions): Redis {
1010
// TODO: Figure out what other options we should set (timeouts, etc)
1111
// TODO: pass on connection error
1212
try {
13-
const client: Redis = new Redis(options);
13+
const client: Redis = new Redis(options); // Default port is 6379 automatically
1414
clients.push(client);
1515
return client;
1616
} catch (err) {

test/middleware/express.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ describe('Express Middleware tests', () => {
283283
refillRate: 1,
284284
capacity: 20,
285285
},
286-
depthLimit: 2,
286+
depthLimit: 3,
287287
});
288288

289289
await middleware(mockRequest as Request, mockResponse as Response, nextFunction);
@@ -496,6 +496,7 @@ describe('Express Middleware tests', () => {
496496
}
497497
} `,
498498
},
499+
ip: '1100',
499500
};
500501

501502
expect(nextFunction).not.toBeCalled();

0 commit comments

Comments
 (0)