Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/api/ai/generate/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { streamText } from "ai";
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { checkAIRateLimit, getRateLimitHeaders } from "@/lib/rate-limit";
import { generateAIActionPrompts } from "@/plugins";

// Simple type for operations
Expand Down Expand Up @@ -256,6 +257,15 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

// Check rate limit
const rateLimit = await checkAIRateLimit(session.user.id);
if (!rateLimit.allowed) {
return NextResponse.json(
{ error: "Rate limit exceeded. Please try again later." },
{ status: 429, headers: getRateLimitHeaders(rateLimit) }
);
}

const body = await request.json();
const { prompt, existingWorkflow } = body;

Expand Down
13 changes: 13 additions & 0 deletions app/api/workflows/[workflowId]/webhook/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { start } from "workflow/api";
import { db } from "@/lib/db";
import { validateWorkflowIntegrations } from "@/lib/db/integrations";
import { apiKeys, workflowExecutions, workflows } from "@/lib/db/schema";
import { checkExecutionRateLimit, getRateLimitHeaders } from "@/lib/rate-limit";
import { executeWorkflow } from "@/lib/workflow-executor.workflow";
import type { WorkflowEdge, WorkflowNode } from "@/lib/workflow-store";

Expand Down Expand Up @@ -149,6 +150,18 @@ export async function POST(
);
}

// Check rate limit
const rateLimit = await checkExecutionRateLimit(workflow.userId);
if (!rateLimit.allowed) {
return NextResponse.json(
{ error: "Rate limit exceeded. Please try again later." },
{
status: 429,
headers: { ...corsHeaders, ...getRateLimitHeaders(rateLimit) },
}
);
}

// Verify this is a webhook-triggered workflow
const triggerNode = (workflow.nodes as WorkflowNode[]).find(
(node) => node.data.type === "trigger"
Expand Down
68 changes: 68 additions & 0 deletions lib/rate-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

type RateLimitResult = {
allowed: boolean;
remaining: number;
resetAt: Date;
};

// Create Redis client with KV_ prefix
const redis = new Redis({
url: process.env.KV_REST_API_URL ?? "",
token: process.env.KV_REST_API_TOKEN ?? "",
Comment on lines +10 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Create Redis client with KV_ prefix
const redis = new Redis({
url: process.env.KV_REST_API_URL ?? "",
token: process.env.KV_REST_API_TOKEN ?? "",
// Validate required environment variables
if (!process.env.KV_REST_API_URL || !process.env.KV_REST_API_TOKEN) {
throw new Error(
"Missing required environment variables for rate limiting: KV_REST_API_URL and KV_REST_API_TOKEN. " +
"Please configure your Upstash Redis credentials in your environment."
);
}
// Create Redis client with KV_ prefix
const redis = new Redis({
url: process.env.KV_REST_API_URL,
token: process.env.KV_REST_API_TOKEN,

Missing validation for required environment variables KV_REST_API_URL and KV_REST_API_TOKEN. Using empty string defaults will cause rate limiting to fail at runtime with unhelpful error messages.

View Details

Analysis

Missing validation for required Upstash Redis environment variables

What fails: Rate limiting fails at runtime when KV_REST_API_URL or KV_REST_API_TOKEN environment variables are not set. The rate limiter module initializes successfully without these credentials (lazy-evaluated), but any attempt to check rate limits throws an unhelpful error.

How to reproduce:

  1. Set KV_REST_API_URL and KV_REST_API_TOKEN to empty/undefined in environment
  2. Start the application (it starts without error)
  3. Make any request that triggers rate limit checking (POST to /api/ai/generate)
  4. Request fails with error caught by try-catch block

Result: Error Failed to parse URL from /pipeline is thrown when aiRatelimit.limit() is called, which gets caught by the route's try-catch and returned as a generic 500 error to the client with message: "Failed to parse URL from /pipeline".

Expected: Module should fail fast at startup with clear error message: "Missing required environment variables for rate limiting: KV_REST_API_URL and KV_REST_API_TOKEN. Please configure your Upstash Redis credentials in your environment."

Why this matters: Without validation at startup, developers see a cryptic error message at runtime instead of knowing exactly which environment variables are missing. This makes debugging deployments much harder. Compare to industry standard practice where required configuration is validated on application startup.

Tested with: @upstash/redis 1.35.7, @upstash/ratelimit 2.0.7 - confirmed that empty string credentials pass initialization but fail on first API call with Invalid URL error.

});

// AI generation: 50 requests per hour
const aiRatelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(50, "1 h"),
prefix: "ratelimit:ai",
});

// Webhook execution: 1000 requests per hour
const webhookRatelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(1000, "1 h"),
prefix: "ratelimit:webhook",
});

/**
* Check rate limit for AI generation requests
*/
export async function checkAIRateLimit(
userId: string
): Promise<RateLimitResult> {
const { success, remaining, reset } = await aiRatelimit.limit(userId);

return {
allowed: success,
remaining,
resetAt: new Date(reset),
};
}

/**
* Check rate limit for workflow executions
*/
export async function checkExecutionRateLimit(
userId: string
): Promise<RateLimitResult> {
const { success, remaining, reset } = await webhookRatelimit.limit(userId);

return {
allowed: success,
remaining,
resetAt: new Date(reset),
};
}

/**
* Get rate limit headers for response
*/
export function getRateLimitHeaders(result: RateLimitResult): HeadersInit {
return {
"X-RateLimit-Remaining": result.remaining.toString(),
"X-RateLimit-Reset": result.resetAt.toISOString(),
};
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@slack/web-api": "^7.12.0",
"@upstash/ratelimit": "^2.0.7",
"@upstash/redis": "^1.35.7",
"@vercel/analytics": "^1.5.0",
"@vercel/og": "^0.8.5",
"@vercel/sdk": "^1.17.1",
Expand Down
36 changes: 34 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.