Skip to content

Commit

Permalink
feat: enable ratelimiting at query level, with dedicated query/unit i…
Browse files Browse the repository at this point in the history
…n the configuration
  • Loading branch information
dbrrt committed Dec 29, 2023
1 parent f68ff69 commit 7959cd6
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 43 deletions.
6 changes: 6 additions & 0 deletions .changeset/wicked-spiders-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@authdog/hydra-core": patch
"@authdog/hydra-cli": patch
---

enable ratelimiting at query level, with dedicated query/unit in the configuration
3 changes: 1 addition & 2 deletions packages/cli/src/utils/validateConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ const hydraConfigSchema = z.object({
export const validateConfig = (config: IHydraConfig): IHydraConfig => {
try {
// Validate the provided config object against the Zod schema
const validatedConfig = hydraConfigSchema.parse(config);
return validatedConfig;
return hydraConfigSchema.parse(config);
} catch (error) {
throw error;
}
Expand Down
67 changes: 48 additions & 19 deletions packages/core/src/handlers/hydra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export const HydraHandler = async (req, env, ctx): Promise<Response> => {

const { kv, hydraConfig, rateLimiter } = ctx;
let cacheKey = null;
// const { RateLimiter } = req;
// get ip address
const ip =
req.headers.get("cf-connecting-ip") ||
Expand All @@ -43,11 +42,10 @@ export const HydraHandler = async (req, env, ctx): Promise<Response> => {
],
};

const facetId = ip;
const facetId = ip || "localhost" // TODO: extend to more facets combinations
const defaultRateLimitingBudget = hydraConfig.rateLimiting.default.budget;
let remainingRateBudget = -1;


const kvNamespace = kv;
const requestHeaders = req.clone()?.headers;
const requestBody = await req.clone()?.json();
Expand Down Expand Up @@ -76,33 +74,64 @@ export const HydraHandler = async (req, env, ctx): Promise<Response> => {
);

if (rateLimiter && !isIntrospection && !isMutation) {

const timestamp = new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "");
const facetQueriesIds = extractedQueries.map((query) => {
return `${facetId}_${query}`;
})

// TODO: generate map query -> rateCount
const rateCounts = await Promise.all(facetQueriesIds.map(async (facetQueryId) => {
const rateCount = await fetchRateLimiterWithFacet(req, rateLimiter, facetQueryId, timestamp);
return rateCount;
}));
const facetQueryId = `${facetId}_${query}`;
const queryId = hydraConfig.rateLimiting?.queries?.find((queryConfig) => {
return queryConfig.id === query;
})?.id || "default";
const queryBudget = hydraConfig.rateLimiting?.queries?.find((queryConfig) => {
return queryConfig.id === query;
})?.budget || defaultRateLimitingBudget;
const queryBudgetUnit = hydraConfig.rateLimiting?.queries?.find((queryConfig) => {
return queryConfig.id === query;
})?.unit || "minute";

return {
facetQueryId,
queryId,
queryBudget,
queryBudgetUnit,
};

})
let hasAtLeastOneExceeded = false;

rateCounts.map((rateCount) => {
// TODO: read from config budget for given query
if (Number(rateCount) > defaultRateLimitingBudget) {
const rateCountsReports = await Promise.all(facetQueriesIds.map(async (facetObj) => {
let timestamp;
if (facetObj.queryBudgetUnit === "hour") {
// generate timestamp removing minutes and seconds
timestamp = new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "").slice(0, 10);
} else {
// generate timestamp removing seconds
timestamp = new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "").slice(0, 12);
}
const rateCount = await fetchRateLimiterWithFacet(req, rateLimiter, facetObj.facetQueryId, timestamp);

if (rateCount > facetObj.queryBudget) {
hasAtLeastOneExceeded = true;
return true;
}
})

return {
facetQueryId: facetObj.facetQueryId,
queryBudget: facetObj.queryBudget,
queryBudgetUnit: facetObj.queryBudgetUnit,
rateCount
}
}));

console.log("rateCountsReports", JSON.stringify(rateCountsReports, null, 2));

const excedeedRateCountReports = rateCountsReports.filter((report) => {
return report.rateCount > report.queryBudget;
});


if (hasAtLeastOneExceeded) {
if (excedeedRateCountReports?.length > 0) {
const errorResponse = {
errors: [
{
message: "Too many requests",
message: `Too many requests for ${excedeedRateCountReports[0]?.facetQueryId}`,
extensions: {
code: "TOO_MANY_REQUESTS",
statusCode: 429,
Expand Down
16 changes: 12 additions & 4 deletions services/itty-hydra/hydra.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@ export const HydraConfigAcme = {
default: {
budget: 20,
},
health: {
budget: 5,
unit: "minute",
}
queries: [
{
id: "health",
budget: 5,
unit: "minute",
},
{
id: "hydraDevQuery",
budget: 15,
unit: "minute",
},
]
},
publicQueries: [
{
Expand Down
36 changes: 18 additions & 18 deletions services/itty-hydra/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,24 @@ router
.get("/health", Health)
.get("/graphql", GraphQLHandler)
.post("/graphql", HydraHandler)
.get("/counter", async (req, { HydraRateLimiter }) => {
try {
const jsonResponse = await fetchWithRateLimiter(
req,
HydraRateLimiter,
"testFacet",
);
return new Response(JSON.stringify(jsonResponse), {
status: 200,
headers: {
"content-type": "text/plain",
},
});
} catch (error) {
// Handle any errors that might occur during the process
return new Response("Internal Server Error", { status: 500 });
}
})
// .get("/counter", async (req, { HydraRateLimiter }) => {
// try {
// const jsonResponse = await fetchWithRateLimiter(
// req,
// HydraRateLimiter,
// "testFacet",
// );
// return new Response(JSON.stringify(jsonResponse), {
// status: 200,
// headers: {
// "content-type": "text/plain",
// },
// });
// } catch (error) {
// // Handle any errors that might occur during the process
// return new Response("Internal Server Error", { status: 500 });
// }
// })
.all("*", NotFound);

const handleRequest = async (req, env, ctx) => {
Expand Down

0 comments on commit 7959cd6

Please sign in to comment.