feat: improve error messages (#1006)

This commit is contained in:
Jeremy 2025-05-13 19:01:28 -07:00 committed by GitHub
parent 9127e1be88
commit 8a7d3e9950
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 370 additions and 174 deletions

View file

@ -32,6 +32,7 @@ import type { ArtifactKind } from '@/components/artifact';
import { generateUUID } from '../utils';
import { generateHashedPassword } from './utils';
import type { VisibilityType } from '@/components/visibility-selector';
import { ChatSDKError } from '../errors';
// Optionally, if not using email/pass login, you can
// use the Drizzle adapter for Auth.js / NextAuth
@ -45,8 +46,10 @@ export async function getUser(email: string): Promise<Array<User>> {
try {
return await db.select().from(user).where(eq(user.email, email));
} catch (error) {
console.error('Failed to get user from database');
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to get user by email',
);
}
}
@ -56,8 +59,7 @@ export async function createUser(email: string, password: string) {
try {
return await db.insert(user).values({ email, password: hashedPassword });
} catch (error) {
console.error('Failed to create user in database');
throw error;
throw new ChatSDKError('bad_request:database', 'Failed to create user');
}
}
@ -71,8 +73,10 @@ export async function createGuestUser() {
email: user.email,
});
} catch (error) {
console.error('Failed to create guest user in database');
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to create guest user',
);
}
}
@ -96,8 +100,7 @@ export async function saveChat({
visibility,
});
} catch (error) {
console.error('Failed to save chat in database');
throw error;
throw new ChatSDKError('bad_request:database', 'Failed to save chat');
}
}
@ -113,8 +116,10 @@ export async function deleteChatById({ id }: { id: string }) {
.returning();
return chatsDeleted;
} catch (error) {
console.error('Failed to delete chat by id from database');
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to delete chat by id',
);
}
}
@ -154,7 +159,10 @@ export async function getChatsByUserId({
.limit(1);
if (!selectedChat) {
throw new Error(`Chat with id ${startingAfter} not found`);
throw new ChatSDKError(
'not_found:database',
`Chat with id ${startingAfter} not found`,
);
}
filteredChats = await query(gt(chat.createdAt, selectedChat.createdAt));
@ -166,7 +174,10 @@ export async function getChatsByUserId({
.limit(1);
if (!selectedChat) {
throw new Error(`Chat with id ${endingBefore} not found`);
throw new ChatSDKError(
'not_found:database',
`Chat with id ${endingBefore} not found`,
);
}
filteredChats = await query(lt(chat.createdAt, selectedChat.createdAt));
@ -181,8 +192,10 @@ export async function getChatsByUserId({
hasMore,
};
} catch (error) {
console.error('Failed to get chats by user from database');
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to get chats by user id',
);
}
}
@ -191,8 +204,7 @@ export async function getChatById({ id }: { id: string }) {
const [selectedChat] = await db.select().from(chat).where(eq(chat.id, id));
return selectedChat;
} catch (error) {
console.error('Failed to get chat by id from database');
throw error;
throw new ChatSDKError('bad_request:database', 'Failed to get chat by id');
}
}
@ -204,8 +216,7 @@ export async function saveMessages({
try {
return await db.insert(message).values(messages);
} catch (error) {
console.error('Failed to save messages in database', error);
throw error;
throw new ChatSDKError('bad_request:database', 'Failed to save messages');
}
}
@ -217,8 +228,10 @@ export async function getMessagesByChatId({ id }: { id: string }) {
.where(eq(message.chatId, id))
.orderBy(asc(message.createdAt));
} catch (error) {
console.error('Failed to get messages by chat id from database', error);
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to get messages by chat id',
);
}
}
@ -249,8 +262,7 @@ export async function voteMessage({
isUpvoted: type === 'up',
});
} catch (error) {
console.error('Failed to upvote message in database', error);
throw error;
throw new ChatSDKError('bad_request:database', 'Failed to vote message');
}
}
@ -258,8 +270,10 @@ export async function getVotesByChatId({ id }: { id: string }) {
try {
return await db.select().from(vote).where(eq(vote.chatId, id));
} catch (error) {
console.error('Failed to get votes by chat id from database', error);
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to get votes by chat id',
);
}
}
@ -289,8 +303,7 @@ export async function saveDocument({
})
.returning();
} catch (error) {
console.error('Failed to save document in database');
throw error;
throw new ChatSDKError('bad_request:database', 'Failed to save document');
}
}
@ -304,8 +317,10 @@ export async function getDocumentsById({ id }: { id: string }) {
return documents;
} catch (error) {
console.error('Failed to get document by id from database');
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to get documents by id',
);
}
}
@ -319,8 +334,10 @@ export async function getDocumentById({ id }: { id: string }) {
return selectedDocument;
} catch (error) {
console.error('Failed to get document by id from database');
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to get document by id',
);
}
}
@ -346,10 +363,10 @@ export async function deleteDocumentsByIdAfterTimestamp({
.where(and(eq(document.id, id), gt(document.createdAt, timestamp)))
.returning();
} catch (error) {
console.error(
'Failed to delete documents by id after timestamp from database',
throw new ChatSDKError(
'bad_request:database',
'Failed to delete documents by id after timestamp',
);
throw error;
}
}
@ -361,8 +378,10 @@ export async function saveSuggestions({
try {
return await db.insert(suggestion).values(suggestions);
} catch (error) {
console.error('Failed to save suggestions in database');
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to save suggestions',
);
}
}
@ -377,10 +396,10 @@ export async function getSuggestionsByDocumentId({
.from(suggestion)
.where(and(eq(suggestion.documentId, documentId)));
} catch (error) {
console.error(
'Failed to get suggestions by document version from database',
throw new ChatSDKError(
'bad_request:database',
'Failed to get suggestions by document id',
);
throw error;
}
}
@ -388,8 +407,10 @@ export async function getMessageById({ id }: { id: string }) {
try {
return await db.select().from(message).where(eq(message.id, id));
} catch (error) {
console.error('Failed to get message by id from database');
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to get message by id',
);
}
}
@ -424,10 +445,10 @@ export async function deleteMessagesByChatIdAfterTimestamp({
);
}
} catch (error) {
console.error(
'Failed to delete messages by id after timestamp from database',
throw new ChatSDKError(
'bad_request:database',
'Failed to delete messages by chat id after timestamp',
);
throw error;
}
}
@ -441,8 +462,10 @@ export async function updateChatVisiblityById({
try {
return await db.update(chat).set({ visibility }).where(eq(chat.id, chatId));
} catch (error) {
console.error('Failed to update chat visibility in database');
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to update chat visibility by id',
);
}
}
@ -470,10 +493,10 @@ export async function getMessageCountByUserId({
return stats?.count ?? 0;
} catch (error) {
console.error(
'Failed to get message count by user id for the last 24 hours from database',
throw new ChatSDKError(
'bad_request:database',
'Failed to get message count by user id',
);
throw error;
}
}
@ -489,8 +512,10 @@ export async function createStreamId({
.insert(stream)
.values({ id: streamId, chatId, createdAt: new Date() });
} catch (error) {
console.error('Failed to create stream id in database');
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to create stream id',
);
}
}
@ -505,7 +530,9 @@ export async function getStreamIdsByChatId({ chatId }: { chatId: string }) {
return streamIds.map(({ id }) => id);
} catch (error) {
console.error('Failed to get stream ids by chat id from database');
throw error;
throw new ChatSDKError(
'bad_request:database',
'Failed to get stream ids by chat id',
);
}
}

132
lib/errors.ts Normal file
View file

@ -0,0 +1,132 @@
export type ErrorType =
| 'bad_request'
| 'unauthorized'
| 'forbidden'
| 'not_found'
| 'rate_limit'
| 'offline';
export type Surface =
| 'chat'
| 'auth'
| 'api'
| 'stream'
| 'database'
| 'history'
| 'vote'
| 'document'
| 'suggestions';
export type ErrorCode = `${ErrorType}:${Surface}`;
export type ErrorVisibility = 'response' | 'log' | 'none';
export const visibilityBySurface: Record<Surface, ErrorVisibility> = {
database: 'log',
chat: 'response',
auth: 'response',
stream: 'response',
api: 'response',
history: 'response',
vote: 'response',
document: 'response',
suggestions: 'response',
};
export class ChatSDKError extends Error {
public type: ErrorType;
public surface: Surface;
public statusCode: number;
constructor(errorCode: ErrorCode, cause?: string) {
super();
const [type, surface] = errorCode.split(':');
this.type = type as ErrorType;
this.cause = cause;
this.surface = surface as Surface;
this.message = getMessageByErrorCode(errorCode);
this.statusCode = getStatusCodeByType(this.type);
}
public toResponse() {
const code: ErrorCode = `${this.type}:${this.surface}`;
const visibility = visibilityBySurface[this.surface];
const { message, cause, statusCode } = this;
if (visibility === 'log') {
console.error({
code,
message,
cause,
});
return Response.json(
{ code: '', message: 'Something went wrong. Please try again later.' },
{ status: statusCode },
);
}
return Response.json({ code, message, cause }, { status: statusCode });
}
}
export function getMessageByErrorCode(errorCode: ErrorCode): string {
if (errorCode.includes('database')) {
return 'An error occurred while executing a database query.';
}
switch (errorCode) {
case 'bad_request:api':
return "The request couldn't be processed. Please check your input and try again.";
case 'unauthorized:auth':
return 'You need to sign in before continuing.';
case 'forbidden:auth':
return 'Your account does not have access to this feature.';
case 'rate_limit:chat':
return 'You have exceeded your maximum number of messages for the day. Please try again later.';
case 'not_found:chat':
return 'The requested chat was not found. Please check the chat ID and try again.';
case 'forbidden:chat':
return 'This chat belongs to another user. Please check the chat ID and try again.';
case 'unauthorized:chat':
return 'You need to sign in to view this chat. Please sign in and try again.';
case 'offline:chat':
return "We're having trouble sending your message. Please check your internet connection and try again.";
case 'not_found:document':
return 'The requested document was not found. Please check the document ID and try again.';
case 'forbidden:document':
return 'This document belongs to another user. Please check the document ID and try again.';
case 'unauthorized:document':
return 'You need to sign in to view this document. Please sign in and try again.';
case 'bad_request:document':
return 'The request to create or update the document was invalid. Please check your input and try again.';
default:
return 'Something went wrong. Please try again later.';
}
}
function getStatusCodeByType(type: ErrorType) {
switch (type) {
case 'bad_request':
return 400;
case 'unauthorized':
return 401;
case 'forbidden':
return 403;
case 'not_found':
return 404;
case 'rate_limit':
return 429;
case 'offline':
return 503;
default:
return 500;
}
}

View file

@ -2,32 +2,44 @@ import type { CoreAssistantMessage, CoreToolMessage, UIMessage } from 'ai';
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import type { Document } from '@/lib/db/schema';
import { ChatSDKError, type ErrorCode } from './errors';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
interface ApplicationError extends Error {
info: string;
status: number;
}
export const fetcher = async (url: string) => {
const res = await fetch(url);
const response = await fetch(url);
if (!res.ok) {
const error = new Error(
'An error occurred while fetching the data.',
) as ApplicationError;
if (!response.ok) {
const { code, cause } = await response.json();
throw new ChatSDKError(code as ErrorCode, cause);
}
error.info = await res.json();
error.status = res.status;
return response.json();
};
export async function fetchWithErrorHandlers(
input: RequestInfo | URL,
init?: RequestInit,
) {
try {
const response = await fetch(input, init);
if (!response.ok) {
const { code, cause } = await response.json();
throw new ChatSDKError(code as ErrorCode, cause);
}
return response;
} catch (error: unknown) {
if (typeof navigator !== 'undefined' && !navigator.onLine) {
throw new ChatSDKError('offline:chat');
}
throw error;
}
return res.json();
};
}
export function getLocalStorage(key: string) {
if (typeof window !== 'undefined') {