Skip to content

Commit 33822d6

Browse files
committed
Count rate limits for GraphQL fields
1 parent 419f71b commit 33822d6

File tree

8 files changed

+150
-5
lines changed

8 files changed

+150
-5
lines changed

library/agent/Agent.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,15 @@ export class Agent {
573573
});
574574
}
575575

576+
onGraphQLFieldRateLimited(
577+
method: string,
578+
path: string,
579+
type: "query" | "mutation",
580+
field: string
581+
) {
582+
this.routes.countGraphQLFieldRateLimited(method, path, type, field);
583+
}
584+
576585
getRoutes() {
577586
return this.routes;
578587
}

library/agent/Routes.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,3 +980,56 @@ t.test("it counts rate limited requests", async (t) => {
980980
},
981981
]);
982982
});
983+
984+
t.test("it counts rate limited GraphQL fields", async (t) => {
985+
const routes = new Routes(200);
986+
routes.addRoute(getContext("POST", "/graphql"));
987+
routes.addGraphQLField("POST", "/graphql", "query", "user");
988+
989+
routes.countGraphQLFieldRateLimited("POST", "/graphql", "query", "user");
990+
991+
t.same(routes.asArray(), [
992+
{
993+
method: "POST",
994+
path: "/graphql",
995+
hits: 1,
996+
rateLimitedCount: 0,
997+
graphql: undefined,
998+
apispec: {},
999+
graphQLSchema: undefined,
1000+
},
1001+
{
1002+
method: "POST",
1003+
path: "/graphql",
1004+
hits: 1,
1005+
rateLimitedCount: 1,
1006+
graphql: { type: "query", name: "user" },
1007+
apispec: {},
1008+
graphQLSchema: undefined,
1009+
},
1010+
]);
1011+
1012+
routes.addGraphQLField("POST", "/graphql", "query", "user");
1013+
routes.countGraphQLFieldRateLimited("POST", "/graphql", "query", "user");
1014+
1015+
t.same(routes.asArray(), [
1016+
{
1017+
method: "POST",
1018+
path: "/graphql",
1019+
hits: 1,
1020+
rateLimitedCount: 0,
1021+
graphql: undefined,
1022+
apispec: {},
1023+
graphQLSchema: undefined,
1024+
},
1025+
{
1026+
method: "POST",
1027+
path: "/graphql",
1028+
hits: 2,
1029+
rateLimitedCount: 2,
1030+
graphql: { type: "query", name: "user" },
1031+
apispec: {},
1032+
graphQLSchema: undefined,
1033+
},
1034+
]);
1035+
});

library/agent/Routes.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,21 @@ export class Routes {
140140
}
141141
}
142142

143+
countGraphQLFieldRateLimited(
144+
method: string,
145+
path: string,
146+
type: "query" | "mutation",
147+
name: string
148+
) {
149+
const key = this.getGraphQLKey(method, path, type, name);
150+
const existing = this.routes.get(key);
151+
152+
if (existing) {
153+
existing.rateLimitedCount++;
154+
return;
155+
}
156+
}
157+
143158
clear() {
144159
this.routes.clear();
145160
}

library/sources/GraphQL.test.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ function getTestContext() {
1111
method: "POST",
1212
url: "http://localhost:4000/graphql",
1313
query: {},
14-
headers: {},
14+
headers: {
15+
"content-type": "application/json",
16+
},
1517
body: { query: '{ getFile(path: "/etc/bashrc") }' },
1618
cookies: {},
1719
routeParams: {},
@@ -96,11 +98,13 @@ t.test("it works", async () => {
9698
await runWithContext(getTestContext(), async () => {
9799
const success = await query("/etc/bashrc");
98100
t.same(success.data!.getFile, "file content");
99-
await query("/etc/bashrc");
100-
await query("/etc/bashrc");
101+
101102
const result = await query("/etc/bashrc");
102103
t.same(result.errors![0].message, "You are rate limited by Zen.");
103104

105+
await query("/etc/bashrc");
106+
await query("/etc/bashrc");
107+
104108
// With operation name
105109
t.same(
106110
await graphql({
@@ -124,6 +128,33 @@ t.test("it works", async () => {
124128
);
125129
});
126130

131+
t.same(agent.getRoutes().asArray(), [
132+
{
133+
method: "POST",
134+
path: "/graphql",
135+
hits: 6,
136+
rateLimitedCount: 3,
137+
graphql: {
138+
type: "query",
139+
name: "getFile",
140+
},
141+
apispec: {},
142+
graphQLSchema: undefined,
143+
},
144+
{
145+
method: "POST",
146+
path: "/graphql",
147+
hits: 1,
148+
rateLimitedCount: 0,
149+
graphql: {
150+
type: "query",
151+
name: "anotherQuery",
152+
},
153+
apispec: {},
154+
graphQLSchema: undefined,
155+
},
156+
]);
157+
127158
// Empty context
128159
await runWithContext({} as Context, async () => {
129160
const response = await query("/etc/bashrc");

library/sources/GraphQL.tools.test.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ t.test("it works", async () => {
6666
method: "POST",
6767
url: "http://localhost:4000/graphql",
6868
query: {},
69-
headers: {},
69+
headers: { "content-type": "application/json" },
7070
body: { query: '{ getFile(path: "/etc/bashrc") }' },
7171
cookies: {},
7272
routeParams: {},
@@ -85,9 +85,27 @@ t.test("it works", async () => {
8585
await runWithContext(context, async () => {
8686
const success = await query("/etc/bashrc");
8787
t.same(success.data.getFile, "file content");
88+
8889
await query("/etc/bashrc");
89-
await query("/etc/bashrc");
90+
9091
const result = await query("/etc/bashrc");
9192
t.same(result.errors[0].message, "You are rate limited by Zen.");
93+
94+
await query("/etc/bashrc");
95+
96+
t.same(agent.getRoutes().asArray(), [
97+
{
98+
method: "POST",
99+
path: "/graphql",
100+
hits: 5,
101+
rateLimitedCount: 2,
102+
graphql: {
103+
type: "query",
104+
name: "getFile",
105+
},
106+
apispec: {},
107+
graphQLSchema: undefined,
108+
},
109+
]);
92110
});
93111
});

library/sources/GraphQL.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,15 @@ export class GraphQL implements Wrapper {
142142
agent.getInspectionStatistics().onRateLimitedRequest();
143143
updateContext(context, "rateLimited", true);
144144

145+
if (context && context.method && context.route) {
146+
agent.onGraphQLFieldRateLimited(
147+
context.method,
148+
context.route,
149+
result.operationType,
150+
result.field.name.value
151+
);
152+
}
153+
145154
return {
146155
errors: [
147156
new this.graphqlModule.GraphQLError("You are rate limited by Zen.", {

library/sources/graphql/shouldRateLimitOperation.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ t.test("it rate limits query", async () => {
105105

106106
t.match(shouldRateLimitOperation(agent, context, args), {
107107
block: true,
108+
operationType: "query",
109+
source: "ip",
110+
remoteAddress: "1.2.3.4",
108111
});
109112
});
110113

@@ -170,5 +173,8 @@ t.test("it rate limits mutation", async () => {
170173

171174
t.match(shouldRateLimitOperation(agent, context, args), {
172175
block: true,
176+
operationType: "mutation",
177+
source: "ip",
178+
remoteAddress: "1.2.3.4",
173179
});
174180
});

library/sources/graphql/shouldRateLimitOperation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ type Result =
1414
field: FieldNode;
1515
source: "ip";
1616
remoteAddress: string;
17+
operationType: "query" | "mutation";
1718
}
1819
| {
1920
block: true;
2021
field: FieldNode;
2122
source: "user";
2223
userId: string;
24+
operationType: "query" | "mutation";
2325
};
2426

2527
export function shouldRateLimitOperation(
@@ -109,6 +111,7 @@ function shouldRateLimitField(
109111
field: field,
110112
source: "ip",
111113
remoteAddress: context.remoteAddress,
114+
operationType: operationType,
112115
};
113116
}
114117
}
@@ -128,6 +131,7 @@ function shouldRateLimitField(
128131
field: field,
129132
source: "user",
130133
userId: context.user.id,
134+
operationType: operationType,
131135
};
132136
}
133137
}

0 commit comments

Comments
 (0)