this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve API error messages, retry logic, and toast rich colors (#15)

## Improve API error handling and query retry behavior

Improves error messages returned from the chats API to be more descriptive, replacing generic "Not found" and "Invalid character" messages with context-specific ones like "Chat not found" and "Character not found".

Adds a `shouldRetryApiError` utility that prevents React Query from retrying requests that fail with 4xx client errors, since these are deterministic failures that won't resolve on retry. Only 5xx server errors and network-level failures will be retried, up to a maximum of 2 attempts.

Removes the special-case 404 handling in `useChat` that was silently swallowing not-found errors and returning `undefined`, allowing the error to propagate normally instead.

Enables `richColors` on the Sonner toast component so that error and success toasts are visually distinct.

authored by

James Blair and committed by
GitHub
ba082183 ad50a76f

+44 -26
+11 -9
apps/api/src/routes/chats.ts
··· 65 65 with: { messages: true }, 66 66 }); 67 67 68 - if (!res) return c.json({ message: "Not found" }, 404); 68 + if (!res) return c.json({ message: "Chat not found" }, 404); 69 69 70 70 return c.json(res, 200); 71 71 }) ··· 82 82 where: and(eq(schema.chats.id, id), eq(schema.chats.userId, userId)), 83 83 }); 84 84 85 - if (!chat) return c.json({ message: "Not found" }, 404); 85 + if (!chat) return c.json({ message: "Chat not found" }, 404); 86 86 87 87 return c.json(chat, 201); 88 88 }) ··· 95 95 where: and(eq(schema.chats.id, id), eq(schema.chats.userId, userId)), 96 96 }); 97 97 98 - if (!chat) return c.json({ message: "Not found" }, 404); 98 + if (!chat) return c.json({ message: "Chat not found" }, 404); 99 99 100 100 const character = characters.find((c) => c.id === chat.characterId); 101 101 102 - if (!character) return c.json({ message: "Invalid character" }, 400); 102 + if (!character) return c.json({ message: "Character not found" }, 400); 103 103 104 104 const messages = await db.query.messages.findMany({ 105 105 where: and( ··· 197 197 where: and(eq(schema.chats.id, id), eq(schema.chats.userId, userId)), 198 198 }); 199 199 200 - if (!chat) return c.json({ message: "Not found" }, 404); 200 + if (!chat) return c.json({ message: "Chat not found" }, 404); 201 201 202 202 const character = characters.find( 203 203 (character) => character.id === chat.characterId, 204 204 ); 205 205 206 - if (!character) return c.json({ message: "Invalid character" }, 400); 206 + if (!character) return c.json({ message: "Character not found" }, 400); 207 207 208 208 const messages = await db.query.messages.findMany({ 209 209 where: and( ··· 279 279 where: and(eq(schema.chats.id, id), eq(schema.chats.userId, userId)), 280 280 }); 281 281 282 - if (!chat) return c.json({ message: "Not found" }, 404); 282 + if (!chat) return c.json({ message: "Chat not found" }, 404); 283 283 284 284 const character = characters.find( 285 285 (character) => character.id === chat.characterId, 286 286 ); 287 287 288 - if (!character) return c.json({ message: "Invalid character" }, 400); 288 + if (!character) { 289 + return c.json({ message: "Character not found" }, 400); 290 + } 289 291 290 292 const messages = await db.query.messages.findMany({ 291 293 where: and( ··· 387 389 where: and(eq(schema.chats.id, id), eq(schema.chats.userId, userId)), 388 390 }); 389 391 390 - if (!chat) return c.json({ message: "Not found" }, 404); 392 + if (!chat) return c.json({ message: "Chat not found" }, 404); 391 393 392 394 return c.json(chat, 200); 393 395 })
+1
apps/web/src/components/ui/sonner.tsx
··· 19 19 error: <OctagonXIcon className="size-4" />, 20 20 loading: <Loader2Icon className="size-4 animate-spin" />, 21 21 }} 22 + richColors 22 23 style={ 23 24 { 24 25 "--normal-bg": "var(--popover)",
+8 -16
apps/web/src/hooks/use-chat.ts
··· 1 1 import { useAuth } from "@clerk/clerk-react"; 2 2 import { useQuery } from "@tanstack/react-query"; 3 3 import { client } from "#/lib/api"; 4 - import { ApiError, expectApiResponse } from "#/lib/api-error"; 4 + import { expectApiResponse } from "#/lib/api-error"; 5 5 import { chatQueryKey } from "#/lib/chat-query"; 6 6 7 7 interface UseChatOptions { ··· 15 15 enabled: isLoaded && isSignedIn, 16 16 queryKey: chatQueryKey(userId, id), 17 17 queryFn: async () => { 18 - try { 19 - const response = await expectApiResponse( 20 - client.api.chats[":id"].$get({ 21 - param: { id }, 22 - }), 23 - "Failed to load chat", 24 - ); 25 - 26 - return response.json(); 27 - } catch (error) { 28 - if (error instanceof ApiError && error.status === 404) { 29 - return undefined; 30 - } 18 + const response = await expectApiResponse( 19 + client.api.chats[":id"].$get({ 20 + param: { id }, 21 + }), 22 + "Failed to load chat", 23 + ); 31 24 32 - throw error; 33 - } 25 + return response.json(); 34 26 }, 35 27 }); 36 28
+12
apps/web/src/lib/api-error.ts
··· 59 59 60 60 return null; 61 61 } 62 + 63 + export function shouldRetryApiError(error: unknown) { 64 + if (!(error instanceof ApiError)) { 65 + return true; 66 + } 67 + 68 + if (error.status === undefined) { 69 + return true; 70 + } 71 + 72 + return error.status >= 500; 73 + }
+12 -1
apps/web/src/lib/query.ts
··· 1 1 import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query"; 2 2 import { toast } from "sonner"; 3 - import { getApiErrorMessage } from "./api-error"; 3 + import { getApiErrorMessage, shouldRetryApiError } from "./api-error"; 4 4 5 5 function toastApiError(error: unknown) { 6 6 const message = getApiErrorMessage(error); ··· 11 11 } 12 12 13 13 export const queryClient = new QueryClient({ 14 + defaultOptions: { 15 + queries: { 16 + retry: (failureCount, error) => { 17 + if (!shouldRetryApiError(error)) { 18 + return false; 19 + } 20 + 21 + return failureCount < 2; 22 + }, 23 + }, 24 + }, 14 25 mutationCache: new MutationCache({ 15 26 onError: (error) => { 16 27 toastApiError(error);