Skip to content

Commit

Permalink
refactor: 馃挕 fixed ratelimit error message toast
Browse files Browse the repository at this point in the history
  • Loading branch information
growupanand committed Mar 2, 2024
1 parent 8376550 commit e887a1b
Show file tree
Hide file tree
Showing 16 changed files with 85 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Loader2, PenLine, Plus, Sparkles } from "lucide-react";
import { z } from "zod";

import { montserrat } from "@/app/fonts";
import { isRateLimitError } from "@/lib/errorHandlers";
import { isRateLimitErrorResponse } from "@/lib/errorHandlers";
import { cn } from "@/lib/utils";
import { createFormSchema } from "@/lib/validations/form";
import { api } from "@/trpc/react";
Expand Down Expand Up @@ -56,7 +56,9 @@ export default function CreateFormButton({ workspace }: Readonly<Props>) {
title: "Unable to create form",
duration: 2000,
variant: "destructive",
description: isRateLimitError(error) ? error.message : undefined,
description: isRateLimitErrorResponse(error)
? error.message
: undefined,
});
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Check, Edit, MoreVertical, Trash } from "lucide-react";

import { ConfirmAction } from "@/components/common/confirmAction";
import Spinner from "@/components/common/spinner";
import { isRateLimitError } from "@/lib/errorHandlers";
import { isRateLimitErrorResponse } from "@/lib/errorHandlers";
import { cn, debounce } from "@/lib/utils";
import { api } from "@/trpc/react";
import CreateFormButton from "./createFormButton";
Expand Down Expand Up @@ -81,7 +81,9 @@ export const WorkspaceHeader = ({ workspace }: Props) => {
title: "Unable to update workspace",
duration: 2000,
variant: "destructive",
description: isRateLimitError(error) ? error.message : undefined,
description: isRateLimitErrorResponse(error)
? error.message
: undefined,
}),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Input } from "@convoform/ui/components/ui/input";
import { toast } from "@convoform/ui/components/ui/use-toast";
import { useQueryClient } from "@tanstack/react-query";

import { isRateLimitError } from "@/lib/errorHandlers";
import { isRateLimitErrorResponse } from "@/lib/errorHandlers";
import { cn, debounce } from "@/lib/utils";
import { api } from "@/trpc/react";
import { ExtractFieldErrors } from "@/trpc/utils";
Expand Down Expand Up @@ -36,7 +36,7 @@ export default function ChangeNameInput({ form, className }: Props) {
title: "Unable to update Form's Name",
duration: 2000,
variant: "destructive",
description: isRateLimitError(error)
description: isRateLimitErrorResponse(error)
? error.message
: nameFieldError ?? undefined,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { z } from "zod";

import { montserrat } from "@/app/fonts";
import { apiClient } from "@/lib/apiClient";
import { isRateLimitError } from "@/lib/errorHandlers";
import { isRateLimitErrorResponse } from "@/lib/errorHandlers";
import { cn } from "@/lib/utils";
import { formUpdateSchema } from "@/lib/validations/form";
import { api } from "@/trpc/react";
Expand Down Expand Up @@ -111,7 +111,9 @@ export function FormEditorCard({ form }: Readonly<Props>) {
title: "Unable to save changes",
duration: 2000,
variant: "destructive",
description: isRateLimitError(error) ? error.message : undefined,
description: isRateLimitErrorResponse(error)
? error.message
: undefined,
});
},
});
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/api/ai/generateForm/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { checkRateLimit } from "@convoform/api";
import { checkRateLimitThrowError } from "@convoform/api";
import { z } from "zod";

import { aiGeneratedFormLimit } from "@/lib/config/pricing";
Expand All @@ -21,7 +21,7 @@ export async function POST(req: NextRequest) {

// TODO: After moving AI related routes to tRPC, we can use userId as identifier

await checkRateLimit({
await checkRateLimitThrowError({
identifier: orgId,
rateLimitType: "ai:identified",
});
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/api/form/[formId]/conversation/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest } from "next/server";
import { checkRateLimit } from "@convoform/api";
import { checkRateLimitThrowError } from "@convoform/api";
import { z } from "zod";

import { formSubmissionLimit } from "@/lib/config/pricing";
Expand Down Expand Up @@ -29,7 +29,7 @@ export async function POST(

const clientIp = getIP(req);

await checkRateLimit({
await checkRateLimitThrowError({
identifier: clientIp ?? "unknown",
rateLimitType: clientIp ? "ai:identified" : "ai:unkown",
});
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/api/form/[formId]/getNextFormField/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { checkRateLimit } from "@convoform/api";
import { checkRateLimitThrowError } from "@convoform/api";

import { sendErrorResponse } from "@/lib/errorHandlers";
import getIP from "@/lib/getIP";
Expand All @@ -15,7 +15,7 @@ export async function POST(req: NextRequest) {

const clientIp = getIP(req);

await checkRateLimit({
await checkRateLimitThrowError({
identifier: clientIp ?? "unknown",
rateLimitType: clientIp ? "ai:identified" : "ai:unkown",
});
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/formSubmissionPage/formViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { sendErrorResponseToast } from "@convoform/ui/components/ui/use-toast";
import { useChat } from "ai/react";

import { CONVERSATION_START_MESSAGE } from "@/lib/constants";
import { isRateLimitError } from "@/lib/errorHandlers";
import { isRateLimitErrorResponse } from "@/lib/errorHandlers";
import { EndScreen } from "./endScreen";
import { FormFieldsViewer } from "./formFields";
import { WelcomeScreen } from "./welcomeScreen";
Expand Down Expand Up @@ -49,7 +49,7 @@ export function FormViewer({ form, refresh, isPreview }: Props) {
onError(error) {
let errorMessage;
try {
if (isRateLimitError(error)) {
if (isRateLimitErrorResponse(error)) {
errorMessage = error.message ?? "Rate limit exceeded";
} else {
errorMessage = JSON.parse(error.message).nonFieldError;
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/mainPage/navigationCardContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { toast } from "@convoform/ui/components/ui/use-toast";
import { useQueryClient } from "@tanstack/react-query";
import { Loader2, Plus } from "lucide-react";

import { isRateLimitError } from "@/lib/errorHandlers";
import { isRateLimitErrorResponse } from "@/lib/errorHandlers";
import { NavigationConfig } from "@/lib/types/navigation";
import { api } from "@/trpc/react";
import BrandName from "../common/brandName";
Expand Down Expand Up @@ -42,7 +42,9 @@ export function NavigationCardContent({ orgId }: Readonly<Props>) {
title: "Unable to create workspace",
duration: 2000,
variant: "destructive",
description: isRateLimitError(error) ? error.message : undefined,
description: isRateLimitErrorResponse(error)
? error.message
: undefined,
}),
});
const isCreatingWorkspace = createWorkspace.isPending;
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/errorHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ export const sendErrorResponse = (error: any) => {
);
};

export const isRateLimitError = (error: any) => {
return error.data?.code === "TOO_MANY_REQUESTS";
export const isRateLimitErrorResponse = (error: any) => {
return error.data?.code === "TOO_MANY_REQUESTS" || error.status === 429;
};
6 changes: 3 additions & 3 deletions apps/web/src/trpc/react.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"use client";

import { useState } from "react";
import type { AppRouter } from "@convoform/api";
import { type AppRouter } from "@convoform/api";
import { toast } from "@convoform/ui/components/ui/use-toast";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import SuperJSON from "superjson";

import { isRateLimitError } from "@/lib/errorHandlers";
import { isRateLimitErrorResponse } from "@/lib/errorHandlers";
import { getTRPCUrl } from "@/lib/url";

export const api = createTRPCReact<AppRouter>();
Expand All @@ -25,7 +25,7 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
},
mutations: {
onError: (err) => {
if (isRateLimitError(err)) {
if (isRateLimitErrorResponse(err)) {
toast({
title: err.message ?? "Too many requests",
duration: 1500,
Expand Down
11 changes: 3 additions & 8 deletions packages/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";

import { checkRateLimit, RATE_LIMIT_ERROR_NAME } from "./src/lib/rateLimit";
import { checkRateLimitThrowError } from "./src/lib/rateLimit";
import type { AppRouter } from "./src/router/root";
import { appRouter } from "./src/router/root";
import { createCallerFactory, createTRPCContext } from "./src/trpc";
Expand Down Expand Up @@ -28,12 +28,7 @@ type RouterInputs = inferRouterInputs<AppRouter>;
* type AllPostsOutput = RouterOutputs['post']['all']
* ^? Post[]
**/

type RouterOutputs = inferRouterOutputs<AppRouter>;
export {
createTRPCContext,
appRouter,
createCaller,
checkRateLimit,
RATE_LIMIT_ERROR_NAME,
};
export { createTRPCContext, appRouter, createCaller, checkRateLimitThrowError };
export type { AppRouter, RouterInputs, RouterOutputs };
47 changes: 42 additions & 5 deletions packages/api/src/lib/rateLimit.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TRPCError } from "@trpc/server";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

Expand All @@ -16,7 +17,9 @@ type LimitType =
| "common"
| "core:create"
| "core:edit"
/** All OpenAI request called (except from loggedInUser or detected ip address of client) */
| "ai:unkown"
/** All OpenAI request called by loggedInUser or detected ip address of client */
| "ai:identified";
type RateLimit = Record<LimitType, any>;
export const RATE_LIMIT_ERROR_NAME = "TOO_MANY_REQUESTS";
Expand Down Expand Up @@ -45,22 +48,30 @@ export const ratelimit = redis
redis,
analytics: true,
prefix: "ratelimit:ai",
limiter: Ratelimit.fixedWindow(300, "1d"),
limiter: Ratelimit.fixedWindow(400, "1d"),
}),
["ai:identified"]: new Ratelimit({
redis,
analytics: true,
prefix: "ratelimit:ai",
limiter: Ratelimit.fixedWindow(100, "1d"),
limiter: Ratelimit.fixedWindow(150, "1d"),
}),
} as RateLimit)
: undefined;

export const isRateLimitError = (error: any) => {
return error.name === RATE_LIMIT_ERROR_NAME;
};

export const isRateLimitErrorResponse = (error: any) => {
return error.data?.code === RATE_LIMIT_ERROR_NAME || error.status === 429;
};

/**
* Check for rate limit and throw an error if the limit is exceeded.
* @returns `Promise<void>`
*/
export const checkRateLimit = async ({
export const checkRateLimitThrowError = async ({
identifier,
message,
rateLimitType,
Expand Down Expand Up @@ -100,6 +111,32 @@ export const checkRateLimit = async ({
}
};

export const isRateLimitError = (error: any) => {
return error.name === RATE_LIMIT_ERROR_NAME;
export const checkRateLimitThrowTRPCError = async ({
identifier,
message,
rateLimitType,
}: {
/** A unique string value to identify user */
identifier: string;
/** Custom message to send in response */
message?: string;
/** Limit type E.g. `core`, `AI` etc */
rateLimitType?: LimitType;
}) => {
try {
await checkRateLimitThrowError({
identifier,
message,
rateLimitType,
});
} catch (error) {
if (error instanceof Error && isRateLimitError(error)) {
throw new TRPCError({
code: RATE_LIMIT_ERROR_NAME,
message: error.message,
cause: error.cause,
});
}
throw error;
}
};
8 changes: 4 additions & 4 deletions packages/api/src/router/form.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { and, count, eq, form, formField } from "@convoform/db";
import { z } from "zod";

import { checkRateLimit } from "../lib/rateLimit";
import { checkRateLimitThrowTRPCError } from "../lib/rateLimit";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import {
formCreateSchema,
Expand All @@ -18,7 +18,7 @@ export const formRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
await checkRateLimit({
await checkRateLimitThrowTRPCError({
identifier: ctx.userId,
rateLimitType: "core:create",
});
Expand Down Expand Up @@ -141,7 +141,7 @@ export const formRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
await checkRateLimit({
await checkRateLimitThrowTRPCError({
identifier: ctx.userId,
rateLimitType: "core:edit",
});
Expand All @@ -159,7 +159,7 @@ export const formRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
await checkRateLimit({
await checkRateLimitThrowTRPCError({
identifier: ctx.userId,
rateLimitType: "core:edit",
});
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/router/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { eq, insertWorkspaceSchema, workspace } from "@convoform/db";
import { z } from "zod";

import { checkRateLimit } from "../lib/rateLimit";
import { checkRateLimitThrowTRPCError } from "../lib/rateLimit";
import { createTRPCRouter, protectedProcedure } from "../trpc";

export const workspaceRouter = createTRPCRouter({
create: protectedProcedure
.input(insertWorkspaceSchema)
.mutation(async ({ input, ctx }) => {
await checkRateLimit({
await checkRateLimitThrowTRPCError({
identifier: ctx.userId,
rateLimitType: "core:create",
});
Expand Down Expand Up @@ -71,7 +71,7 @@ export const workspaceRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
await checkRateLimit({
await checkRateLimitThrowTRPCError({
identifier: ctx.userId,
rateLimitType: "core:edit",
});
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/components/ui/use-toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ async function sendErrorResponseToast(
}
toast({
title: errorMessage || fallbackMessage || "Something went wrong",
duration: 1500,
duration: 2000,
variant: "destructive",
});
}

Expand Down

0 comments on commit e887a1b

Please sign in to comment.