-
Notifications
You must be signed in to change notification settings - Fork 11
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
chore!: deprecate reason
for ArcjetAllowDecision
#2715
base: main
Are you sure you want to change the base?
Changes from 5 commits
2451659
8b78916
3173d91
e3fc641
5b25ae9
312655b
0caefa4
a6dcec4
af75332
9fc8d23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import arcjet, { ArcjetDecision, tokenBucket, shield } from "@arcjet/next"; | ||
import arcjet, { ArcjetDecision, tokenBucket, shield, ArcjetRateLimitReason, ArcjetReason, ArcjetRuleResult } from "@arcjet/next"; | ||
import format from "@arcjet/sprintf"; | ||
import { NextRequest, NextResponse } from "next/server"; | ||
import { currentUser } from "@clerk/nextjs/server"; | ||
import ip from "@arcjet/ip"; | ||
|
@@ -16,6 +17,47 @@ const aj = arcjet({ | |
], | ||
}); | ||
|
||
function extractReason(result: ArcjetRuleResult): ArcjetReason { | ||
return result.reason; | ||
} | ||
|
||
function isRateLimitReason( | ||
reason?: ArcjetReason, | ||
): reason is ArcjetRateLimitReason { | ||
return typeof reason !== "undefined" && reason.isRateLimit(); | ||
} | ||
|
||
function nearestLimit( | ||
current: ArcjetRateLimitReason, | ||
next: ArcjetRateLimitReason, | ||
) { | ||
if (current.remaining < next.remaining) { | ||
return current; | ||
} | ||
|
||
if (current.remaining > next.remaining) { | ||
return next; | ||
} | ||
|
||
// Reaching here means `remaining` is equal so prioritize closest reset | ||
if (current.reset < next.reset) { | ||
return current; | ||
} | ||
|
||
if (current.reset > next.reset) { | ||
return next; | ||
} | ||
|
||
// Reaching here means that `remaining` and `reset` are equal, so prioritize | ||
// the smallest `max` | ||
if (current.max < next.max) { | ||
return current; | ||
} | ||
|
||
// All else equal, just return the next item in the list | ||
return next; | ||
} | ||
|
||
export async function GET(req: NextRequest) { | ||
// Get the current user from Clerk | ||
// See https://clerk.com/docs/references/nextjs/current-user | ||
|
@@ -69,13 +111,40 @@ export async function GET(req: NextRequest) { | |
); | ||
} | ||
} | ||
|
||
let reset: Date | undefined; | ||
let remaining: number | undefined; | ||
|
||
if (decision.reason.isRateLimit()) { | ||
reset = decision.reason.resetTime; | ||
remaining = decision.reason.remaining; | ||
const rateLimitReasons = decision.results | ||
.map(extractReason) | ||
.filter(isRateLimitReason); | ||
|
||
if (rateLimitReasons.length > 0) { | ||
const policies = new Map<number, number>(); | ||
for (const reason of rateLimitReasons) { | ||
if (policies.has(reason.max)) { | ||
console.error( | ||
"Invalid rate limit policy—two policies should not share the same limit", | ||
); | ||
} | ||
|
||
if ( | ||
typeof reason.max !== "number" || | ||
typeof reason.window !== "number" || | ||
typeof reason.remaining !== "number" || | ||
typeof reason.reset !== "number" | ||
) { | ||
console.error(format("Invalid rate limit encountered: %o", reason)); | ||
} | ||
|
||
policies.set(reason.max, reason.window); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Policies isn't doing anything? Did you want to break out of the logic or something? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Its used to print an error if two rate limit policies share the same limit, like we do in decorate. The difference is that decorate returns an error in this case. We could respond with a 500 here to have the same behavior. What do you think? Another option would be to detect invalid configs earlier, such as when initializing Arcjet. But that would be a much larger change. |
||
const rl = rateLimitReasons.reduce(nearestLimit); | ||
|
||
reset = rl.resetTime; | ||
remaining = rl.remaining; | ||
} | ||
|
||
return NextResponse.json({ message: "Hello World", reset, remaining });} | ||
return NextResponse.json({ message: "Hello World", reset, remaining }); | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
}, | ||
"dependencies": { | ||
"@arcjet/next": "file:../../arcjet-next", | ||
"@arcjet/sprintf": "file:../../sprintf", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might not be needed if you don't keep the policies stuff |
||
"@clerk/nextjs": "^6.9.1", | ||
"next": "15.1.0", | ||
"react": "^19", | ||
|
@@ -26,4 +27,4 @@ | |
"tailwindcss": "^3.4.16", | ||
"typescript": "^5" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,8 @@ | |
adding custom characteristics so you could use a user ID to track authenticated | ||
users instead. See the `chat_userid` example for an example of this. | ||
*/ | ||
import arcjet, { shield, tokenBucket } from "@arcjet/next"; | ||
import arcjet, { ArcjetRateLimitReason, ArcjetReason, ArcjetRuleResult, shield, tokenBucket } from "@arcjet/next"; | ||
import format from "@arcjet/sprintf"; | ||
import { openai } from '@ai-sdk/openai'; | ||
import { streamText } from 'ai'; | ||
import { promptTokensEstimate } from "openai-chat-tokens"; | ||
|
@@ -39,6 +40,47 @@ const aj = arcjet({ | |
], | ||
}); | ||
|
||
function extractReason(result: ArcjetRuleResult): ArcjetReason { | ||
return result.reason; | ||
} | ||
|
||
function isRateLimitReason( | ||
reason?: ArcjetReason, | ||
): reason is ArcjetRateLimitReason { | ||
return typeof reason !== "undefined" && reason.isRateLimit(); | ||
} | ||
|
||
function nearestLimit( | ||
current: ArcjetRateLimitReason, | ||
next: ArcjetRateLimitReason, | ||
) { | ||
if (current.remaining < next.remaining) { | ||
return current; | ||
} | ||
|
||
if (current.remaining > next.remaining) { | ||
return next; | ||
} | ||
|
||
// Reaching here means `remaining` is equal so prioritize closest reset | ||
if (current.reset < next.reset) { | ||
return current; | ||
} | ||
|
||
if (current.reset > next.reset) { | ||
return next; | ||
} | ||
|
||
// Reaching here means that `remaining` and `reset` are equal, so prioritize | ||
// the smallest `max` | ||
if (current.max < next.max) { | ||
return current; | ||
} | ||
|
||
// All else equal, just return the next item in the list | ||
return next; | ||
} | ||
|
||
// Allow streaming responses up to 30 seconds | ||
export const maxDuration = 30; | ||
|
||
|
@@ -48,7 +90,7 @@ export const runtime = "edge"; | |
export async function POST(req: Request) { | ||
const { messages } = await req.json(); | ||
|
||
// Estimate the number of tokens required to process the request | ||
// Estimate the number of tokens required to process the request | ||
const estimate = promptTokensEstimate({ | ||
messages, | ||
}); | ||
|
@@ -59,8 +101,40 @@ export async function POST(req: Request) { | |
const decision = await aj.protect(req, { requested: estimate }); | ||
console.log("Arcjet decision", decision.conclusion); | ||
|
||
if (decision.reason.isRateLimit()) { | ||
console.log("Requests remaining", decision.reason.remaining); | ||
const rateLimitReasons = decision.results | ||
.map(extractReason) | ||
.filter(isRateLimitReason); | ||
|
||
let remaining: number | undefined; | ||
|
||
if (rateLimitReasons.length > 0) { | ||
const policies = new Map<number, number>(); | ||
for (const reason of rateLimitReasons) { | ||
if (policies.has(reason.max)) { | ||
console.error( | ||
"Invalid rate limit policy—two policies should not share the same limit", | ||
); | ||
} | ||
|
||
if ( | ||
typeof reason.max !== "number" || | ||
typeof reason.window !== "number" || | ||
typeof reason.remaining !== "number" || | ||
typeof reason.reset !== "number" | ||
) { | ||
console.error(format("Invalid rate limit encountered: %o", reason)); | ||
} | ||
|
||
policies.set(reason.max, reason.window); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see above |
||
} | ||
|
||
const rl = rateLimitReasons.reduce(nearestLimit); | ||
|
||
remaining = rl.remaining; | ||
} | ||
|
||
if (typeof remaining !== "undefined") { | ||
console.log("Requests remaining", remaining); | ||
} | ||
|
||
// If the request is denied, return a 429 | ||
|
@@ -83,4 +157,4 @@ export async function POST(req: Request) { | |
}); | ||
|
||
return result.toDataStreamResponse(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed you are still using map/filter in the other examples (for rate limit). We should keep it uniform. The reason to use a reduce is to avoid creating extra arrays, but the implementation of the reduction function is creating a new array on each iteration which is worse than a single filter then a map.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed this to a reduce. This removes checking that a limit isn't duplicated which is discussed in another comment though.