馃 distributed transcription service
thistle.dunkirk.sh
1// Structured error codes for API responses
2
3export enum ErrorCode {
4 // Authentication errors
5 AUTH_REQUIRED = "AUTH_REQUIRED",
6 INVALID_SESSION = "INVALID_SESSION",
7 INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
8 EMAIL_ALREADY_EXISTS = "EMAIL_ALREADY_EXISTS",
9
10 // Validation errors
11 VALIDATION_FAILED = "VALIDATION_FAILED",
12 MISSING_FIELD = "MISSING_FIELD",
13 INVALID_FORMAT = "INVALID_FORMAT",
14 FILE_TOO_LARGE = "FILE_TOO_LARGE",
15 UNSUPPORTED_FILE_TYPE = "UNSUPPORTED_FILE_TYPE",
16
17 // Transcription errors
18 TRANSCRIPTION_NOT_FOUND = "TRANSCRIPTION_NOT_FOUND",
19 TRANSCRIPTION_FAILED = "TRANSCRIPTION_FAILED",
20 WHISPER_SERVICE_UNAVAILABLE = "WHISPER_SERVICE_UNAVAILABLE",
21 WHISPER_SERVICE_ERROR = "WHISPER_SERVICE_ERROR",
22 UPLOAD_FAILED = "UPLOAD_FAILED",
23
24 // Session errors
25 SESSION_NOT_FOUND = "SESSION_NOT_FOUND",
26 SESSION_REVOKE_FAILED = "SESSION_REVOKE_FAILED",
27
28 // User errors
29 USER_UPDATE_FAILED = "USER_UPDATE_FAILED",
30 USER_DELETE_FAILED = "USER_DELETE_FAILED",
31
32 // Generic errors
33 INTERNAL_ERROR = "INTERNAL_ERROR",
34 NOT_FOUND = "NOT_FOUND",
35}
36
37export interface ApiError {
38 error: string;
39 code: ErrorCode;
40 details?: string;
41 field?: string;
42}
43
44export class AppError extends Error {
45 constructor(
46 public code: ErrorCode,
47 message: string,
48 public statusCode: number = 500,
49 public details?: string,
50 public field?: string,
51 ) {
52 super(message);
53 this.name = "AppError";
54 }
55
56 toJSON(): ApiError {
57 return {
58 error: this.message,
59 code: this.code,
60 details: this.details,
61 field: this.field,
62 };
63 }
64
65 toResponse(): Response {
66 return Response.json(this.toJSON(), { status: this.statusCode });
67 }
68}
69
70// Helper functions for common errors
71export const AuthErrors = {
72 required: () =>
73 new AppError(ErrorCode.AUTH_REQUIRED, "Authentication required", 401),
74 invalidSession: () =>
75 new AppError(ErrorCode.INVALID_SESSION, "Invalid or expired session", 401),
76 invalidCredentials: () =>
77 new AppError(
78 ErrorCode.INVALID_CREDENTIALS,
79 "Invalid email or password",
80 401,
81 ),
82 emailExists: () =>
83 new AppError(
84 ErrorCode.EMAIL_ALREADY_EXISTS,
85 "Email already registered",
86 400,
87 ),
88};
89
90export const ValidationErrors = {
91 missingField: (field: string) =>
92 new AppError(
93 ErrorCode.MISSING_FIELD,
94 `${field} is required`,
95 400,
96 undefined,
97 field,
98 ),
99 invalidFormat: (field: string, details?: string) =>
100 new AppError(
101 ErrorCode.INVALID_FORMAT,
102 `Invalid ${field} format`,
103 400,
104 details,
105 field,
106 ),
107 fileTooLarge: (maxSize: string) =>
108 new AppError(
109 ErrorCode.FILE_TOO_LARGE,
110 `File size must be less than ${maxSize}`,
111 400,
112 ),
113 unsupportedFileType: (supportedTypes: string) =>
114 new AppError(
115 ErrorCode.UNSUPPORTED_FILE_TYPE,
116 `Unsupported file type. Supported: ${supportedTypes}`,
117 400,
118 ),
119};
120
121export const TranscriptionErrors = {
122 notFound: () =>
123 new AppError(
124 ErrorCode.TRANSCRIPTION_NOT_FOUND,
125 "Transcription not found",
126 404,
127 ),
128 failed: (details?: string) =>
129 new AppError(
130 ErrorCode.TRANSCRIPTION_FAILED,
131 "Transcription failed",
132 500,
133 details,
134 ),
135 serviceUnavailable: () =>
136 new AppError(
137 ErrorCode.WHISPER_SERVICE_UNAVAILABLE,
138 "Transcription service unavailable",
139 503,
140 "The Whisper transcription service is not responding. Please try again later.",
141 ),
142 serviceError: (details: string) =>
143 new AppError(
144 ErrorCode.WHISPER_SERVICE_ERROR,
145 "Transcription service error",
146 502,
147 details,
148 ),
149 uploadFailed: (details?: string) =>
150 new AppError(ErrorCode.UPLOAD_FAILED, "Upload failed", 500, details),
151};
152
153export const SessionErrors = {
154 notFound: () =>
155 new AppError(ErrorCode.SESSION_NOT_FOUND, "Session not found", 404),
156 revokeFailed: () =>
157 new AppError(
158 ErrorCode.SESSION_REVOKE_FAILED,
159 "Failed to revoke session",
160 500,
161 ),
162};
163
164export const UserErrors = {
165 updateFailed: (field: string, details?: string) =>
166 new AppError(
167 ErrorCode.USER_UPDATE_FAILED,
168 `Failed to update ${field}`,
169 500,
170 details,
171 ),
172 deleteFailed: () =>
173 new AppError(ErrorCode.USER_DELETE_FAILED, "Failed to delete user", 500),
174};
175
176// Generic error handler
177export function handleError(error: unknown): Response {
178 if (error instanceof AppError) {
179 return error.toResponse();
180 }
181
182 // Handle database unique constraint errors
183 if (
184 error instanceof Error &&
185 error.message?.includes("UNIQUE constraint failed")
186 ) {
187 if (error.message.includes("email")) {
188 return AuthErrors.emailExists().toResponse();
189 }
190 }
191
192 // Log unexpected errors
193 console.error("Unexpected error:", error);
194
195 // Return generic error
196 return new AppError(
197 ErrorCode.INTERNAL_ERROR,
198 "An unexpected error occurred",
199 500,
200 ).toResponse();
201}