馃 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 adminRequired: () =>
89 new AppError(ErrorCode.AUTH_REQUIRED, "Admin access required", 403),
90};
91
92export const ValidationErrors = {
93 missingField: (field: string) =>
94 new AppError(
95 ErrorCode.MISSING_FIELD,
96 `${field} is required`,
97 400,
98 undefined,
99 field,
100 ),
101 invalidFormat: (field: string, details?: string) =>
102 new AppError(
103 ErrorCode.INVALID_FORMAT,
104 `Invalid ${field} format`,
105 400,
106 details,
107 field,
108 ),
109 fileTooLarge: (maxSize: string) =>
110 new AppError(
111 ErrorCode.FILE_TOO_LARGE,
112 `File size must be less than ${maxSize}`,
113 400,
114 ),
115 unsupportedFileType: (supportedTypes: string) =>
116 new AppError(
117 ErrorCode.UNSUPPORTED_FILE_TYPE,
118 `Unsupported file type. Supported: ${supportedTypes}`,
119 400,
120 ),
121};
122
123export const TranscriptionErrors = {
124 notFound: () =>
125 new AppError(
126 ErrorCode.TRANSCRIPTION_NOT_FOUND,
127 "Transcription not found",
128 404,
129 ),
130 failed: (details?: string) =>
131 new AppError(
132 ErrorCode.TRANSCRIPTION_FAILED,
133 "Transcription failed",
134 500,
135 details,
136 ),
137 serviceUnavailable: () =>
138 new AppError(
139 ErrorCode.WHISPER_SERVICE_UNAVAILABLE,
140 "Transcription service unavailable",
141 503,
142 "The Whisper transcription service is not responding. Please try again later.",
143 ),
144 serviceError: (details: string) =>
145 new AppError(
146 ErrorCode.WHISPER_SERVICE_ERROR,
147 "Transcription service error",
148 502,
149 details,
150 ),
151 uploadFailed: (details?: string) =>
152 new AppError(ErrorCode.UPLOAD_FAILED, "Upload failed", 500, details),
153};
154
155export const SessionErrors = {
156 notFound: () =>
157 new AppError(ErrorCode.SESSION_NOT_FOUND, "Session not found", 404),
158 revokeFailed: () =>
159 new AppError(
160 ErrorCode.SESSION_REVOKE_FAILED,
161 "Failed to revoke session",
162 500,
163 ),
164};
165
166export const UserErrors = {
167 updateFailed: (field: string, details?: string) =>
168 new AppError(
169 ErrorCode.USER_UPDATE_FAILED,
170 `Failed to update ${field}`,
171 500,
172 details,
173 ),
174 deleteFailed: () =>
175 new AppError(ErrorCode.USER_DELETE_FAILED, "Failed to delete user", 500),
176};
177
178// Generic error handler
179export function handleError(error: unknown): Response {
180 if (error instanceof AppError) {
181 return error.toResponse();
182 }
183
184 // Handle database unique constraint errors
185 if (
186 error instanceof Error &&
187 error.message?.includes("UNIQUE constraint failed")
188 ) {
189 if (error.message.includes("email")) {
190 return AuthErrors.emailExists().toResponse();
191 }
192 }
193
194 // Log unexpected errors
195 console.error("Unexpected error:", error);
196
197 // Return generic error
198 return new AppError(
199 ErrorCode.INTERNAL_ERROR,
200 "An unexpected error occurred",
201 500,
202 ).toResponse();
203}