rate limits for demo (#695)

This commit is contained in:
Chris Tate
2026-03-09 15:43:25 -05:00
committed by GitHub
parent cc3c70dc86
commit c0a525c9e4
8 changed files with 188 additions and 18 deletions
+42
View File
@@ -1,7 +1,31 @@
"use server";
import { headers } from "next/headers";
import * as serverless from "@/lib/agent-browser";
import * as sandbox from "@/lib/agent-browser-sandbox";
import { ALLOWED_URLS } from "@/lib/constants";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
async function checkRateLimit(): Promise<string | null> {
const h = await headers();
const ip = h.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
const minute = await minuteRateLimit.limit(ip);
if (!minute.success) {
return "Too many requests. Please wait a moment before trying again.";
}
const daily = await dailyRateLimit.limit(ip);
if (!daily.success) {
return "Daily limit reached. Please try again tomorrow.";
}
return null;
}
function isAllowedUrl(url: string): boolean {
return (ALLOWED_URLS as readonly string[]).includes(url);
}
export type EnvStatus = {
serverless: {
@@ -52,6 +76,15 @@ export async function takeScreenshot(
url: string,
mode: Mode = "serverless",
): Promise<ScreenshotResult> {
if (!isAllowedUrl(url)) {
return { ok: false, error: "URL not allowed" };
}
const rateLimitError = await checkRateLimit();
if (rateLimitError) {
return { ok: false, error: rateLimitError };
}
try {
if (mode === "sandbox") {
const { screenshot, title } = await sandbox.screenshotUrl(url);
@@ -78,6 +111,15 @@ export async function takeSnapshot(
url: string,
mode: Mode = "serverless",
): Promise<SnapshotResult> {
if (!isAllowedUrl(url)) {
return { ok: false, error: "URL not allowed" };
}
const rateLimitError = await checkRateLimit();
if (rateLimitError) {
return { ok: false, error: rateLimitError };
}
try {
if (mode === "sandbox") {
const { snapshot, title } = await sandbox.snapshotUrl(url, {
+25 -9
View File
@@ -1,17 +1,29 @@
import { NextRequest, NextResponse } from "next/server";
import * as ab from "@/lib/agent-browser";
import { ALLOWED_URLS } from "@/lib/constants";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
/**
* POST /api/browse
*
* Programmatic API route for browser automation.
* Uses @sparticuz/chromium + puppeteer-core directly in the function.
*
* Body: { "action": "screenshot", "url": "https://example.com" }
* Or: { "action": "snapshot", "url": "https://example.com" }
*/
export async function POST(req: NextRequest) {
try {
const ip =
req.headers.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
const minute = await minuteRateLimit.limit(ip);
if (!minute.success) {
return NextResponse.json(
{ error: "Too many requests. Please wait a moment before trying again." },
{ status: 429 },
);
}
const daily = await dailyRateLimit.limit(ip);
if (!daily.success) {
return NextResponse.json(
{ error: "Daily limit reached. Please try again tomorrow." },
{ status: 429 },
);
}
const body = await req.json();
const url = body.url;
@@ -19,6 +31,10 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "Provide a 'url'" }, { status: 400 });
}
if (!(ALLOWED_URLS as readonly string[]).includes(url)) {
return NextResponse.json({ error: "URL not allowed" }, { status: 400 });
}
if (body.action === "screenshot") {
const result = await ab.screenshotUrl(url, {
fullPage: body.fullPage,
+13 -9
View File
@@ -13,8 +13,8 @@ import {
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable";
import { ALLOWED_URLS } from "@/lib/constants";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
@@ -179,7 +179,7 @@ function ModeCard({
export default function Home() {
const isMobile = useIsMobile();
const [url, setUrl] = useState("https://example.com");
const [url, setUrl] = useState<string>(ALLOWED_URLS[0]);
const [loading, setLoading] = useState(false);
const [action, setAction] = useState<Action>("screenshot");
const [mode, setMode] = useState<Mode>("serverless");
@@ -238,22 +238,26 @@ export default function Home() {
<form onSubmit={handleSubmit} className="p-5 space-y-5">
<div className="space-y-1.5">
<Label
htmlFor="url-input"
htmlFor="url-select"
className="text-[11px] text-muted-foreground uppercase tracking-wider"
>
URL
</Label>
<Input
id="url-input"
type="url"
<select
id="url-select"
value={url}
onChange={(e) => {
setUrl(e.target.value);
clearResults();
}}
placeholder="https://example.com"
required
/>
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{ALLOWED_URLS.map((u) => (
<option key={u} value={u}>
{u.replace("https://", "")}
</option>
))}
</select>
</div>
<div className="space-y-1.5">