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