Support for Next.js server actions
Server actions are the recommended way to do things like handling forms in Next.js. They don't act like route handlers and there is no request object to pass to the Arcjet protect() function.
We can get the headers using the headers function, but the main request details currently needs to be constructed manually. I hacked together a workaround for a form handler, but we should support this in a nicer way
import arcjet, { validateEmail } from "@arcjet/next";
import { headers } from "next/headers";
//import { ipAddress } from '@vercel/functions';
// Utility function to get the IP address of the client
// From https://nextjs.org/docs/app/api-reference/functions/headers#ip-address
function IP() {
const FALLBACK_IP_ADDRESS = "127.0.0.1";
// Uncomment if running on Vercel
//const ip = ipAddress(request)
//return ip ?? FALLBACK_IP_ADDRESS;
const forwardedFor = headers().get("x-forwarded-for");
if (forwardedFor) {
return forwardedFor.split(",")[0] ?? FALLBACK_IP_ADDRESS;
}
return headers().get("x-real-ip") ?? FALLBACK_IP_ADDRESS;
}
// Arcjet client defined outside of the component for example purposes. You
// probably want to put this somewhere else in your app.
const aj = arcjet({
key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
rules: [
validateEmail({
mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
block: ["DISPOSABLE", "NO_MX_RECORDS"], // block disposable email addresses
}),
],
});
// Test form component
export default function Form() {
async function testForm(formData: FormData) {
"use server";
const headersList = headers();
const host = headersList.get("host");
// Construct the request needed by Arcjet because we're not in a request
// handler here.
const path = new URL(`http://${host}`);
const req = {
ip: IP(),
method: "POST",
host: headersList.get("host"),
url: path.href,
headers: headersList,
};
const email = formData.get("email") as string;
const decision = await aj.protect(req, {
email,
});
console.log("Decision: ", decision);
}
return (
<form action={testForm}>
<input type="email" name="email" />
<button type="submit">Submit</button>
</form>
);
}
zsa provides a nice typed wrapper for creating server actions which would be good to provide an example for.
It looks like Vercel sets the x-real-ip header so we can add an "official check" in our library. We should find the documentation from Vercel that states they always override it.
Looking at this again, I don't think this is going to work. new URL(`http://${host}`); doesn't give us the correct path since the URL is only constructed with a host.
Besides headers and IP via headers, I don't see any way to get the stuff we'd need for an "Arcjet Request". That being said, most of values are only required when you configure in a certain way. We could reduce those even further (for example, remove the match option on rate limit rules so we don't need a path to match).
If we want to do the above, we'd need to change the service to better handle lack of data since it currently fails immediately if things don't validate.
We need to support server actions because it's the recommended way for handling things like forms, so we'll need to work on these changes.
You guys have an official integration in Vercel. Maybe you could propose to them someway of accessing the request object within a Next.js Server Action.
Thanks @empz, we'll ping our contact there. We also need a generic solution though because we support Next.js wherever it's hosted.