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