馃 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}