diff --git a/packages/cli/package.json b/packages/cli/package.json index 6a5e097144777..14b3b8f5f873c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -161,6 +161,7 @@ "passport": "^0.6.0", "passport-cookie": "^1.0.9", "passport-jwt": "^4.0.0", + "picocolors": "^1.0.0", "pg": "^8.3.0", "posthog-node": "^1.3.0", "prom-client": "^13.1.0", diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 5748839894d96..2e2fdc69ce6a6 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -191,10 +191,8 @@ export class ActiveWorkflowRunner { ): Promise { Logger.debug(`Received webhook "${httpMethod}" for path "${path}"`); if (this.activeWorkflows === null) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( 'The "activeWorkflows" instance did not get initialized yet.', - 404, - 404, ); } @@ -224,10 +222,8 @@ export class ActiveWorkflowRunner { }); if (dynamicWebhooks === undefined || dynamicWebhooks.length === 0) { // The requested webhook is not registered - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( `The requested webhook "${httpMethod} ${path}" is not registered.`, - 404, - 404, WEBHOOK_PROD_UNREGISTERED_HINT, ); } @@ -252,10 +248,8 @@ export class ActiveWorkflowRunner { } }); if (webhook === undefined) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( `The requested webhook "${httpMethod} ${path}" is not registered.`, - 404, - 404, WEBHOOK_PROD_UNREGISTERED_HINT, ); } @@ -277,10 +271,8 @@ export class ActiveWorkflowRunner { relations: ['shared', 'shared.user', 'shared.user.globalRole'], }); if (workflowData === undefined) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( `Could not find workflow with id "${webhook.workflowId}"`, - 404, - 404, ); } @@ -313,7 +305,7 @@ export class ActiveWorkflowRunner { const workflowStartNode = workflow.getNode(webhookData.node); if (workflowStartNode === null) { - throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404); + throw new ResponseHelper.NotFoundError('Could not find node to process webhook.'); } return new Promise((resolve, reject) => { diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts index e78ae0faac7ce..b04ad0a1de547 100644 --- a/packages/cli/src/GenericHelpers.ts +++ b/packages/cli/src/GenericHelpers.ts @@ -212,7 +212,7 @@ export async function validateEntity( .join(' | '); if (errorMessages) { - throw new ResponseHelper.ResponseError(errorMessages, undefined, 400); + throw new ResponseHelper.BadRequestError(errorMessages); } } diff --git a/packages/cli/src/ResponseHelper.ts b/packages/cli/src/ResponseHelper.ts index 73fd09ce6a6e7..01984a09f7a21 100644 --- a/packages/cli/src/ResponseHelper.ts +++ b/packages/cli/src/ResponseHelper.ts @@ -5,7 +5,8 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Request, Response } from 'express'; import { parse, stringify } from 'flatted'; -import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; +import picocolors from 'picocolors'; +import { ErrorReporterProxy as ErrorReporter, NodeApiError } from 'n8n-workflow'; import type { IExecutionDb, @@ -15,44 +16,69 @@ import type { IWorkflowDb, } from './Interfaces'; +const inDevelopment = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; + /** * Special Error which allows to return also an error code and http status code - * - * @class ResponseError - * @extends {Error} */ -export class ResponseError extends Error { - // The HTTP status code of response - httpStatusCode?: number; - - // The error code in the response - errorCode?: number; - - // The error hint the response - hint?: string; - +abstract class ResponseError extends Error { /** * Creates an instance of ResponseError. * Must be used inside a block with `ResponseHelper.send()`. - * - * @param {string} message The error message - * @param {number} [errorCode] The error code which can be used by frontend to identify the actual error - * @param {number} [httpStatusCode] The HTTP status code the response should have - * @param {string} [hint] The error hint to provide a context (webhook related) */ - constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?: string) { + constructor( + message: string, + // The HTTP status code of response + readonly httpStatusCode: number, + // The error code in the response + readonly errorCode: number = httpStatusCode, + // The error hint the response + readonly hint: string | undefined = undefined, + ) { super(message); this.name = 'ResponseError'; + } +} - if (errorCode) { - this.errorCode = errorCode; - } - if (httpStatusCode) { - this.httpStatusCode = httpStatusCode; - } - if (hint) { - this.hint = hint; - } +export class BadRequestError extends ResponseError { + constructor(message: string) { + super(message, 400); + } +} + +export class AuthError extends ResponseError { + constructor(message: string) { + super(message, 401); + } +} + +export class UnauthorizedError extends ResponseError { + constructor(message: string, hint: string | undefined = undefined) { + super(message, 403, 403, hint); + } +} + +export class NotFoundError extends ResponseError { + constructor(message: string, hint: string | undefined = undefined) { + super(message, 404, 404, hint); + } +} + +export class ConflictError extends ResponseError { + constructor(message: string, hint: string | undefined = undefined) { + super(message, 409, 409, hint); + } +} + +export class InternalServerError extends ResponseError { + constructor(message: string, errorCode = 500) { + super(message, 500, errorCode); + } +} + +export class ServiceUnavailableError extends ResponseError { + constructor(message: string, errorCode = 503) { + super(message, 503, errorCode); } } @@ -95,40 +121,49 @@ export function sendSuccessResponse( } } -export function sendErrorResponse(res: Response, error: ResponseError) { - let httpStatusCode = 500; - if (error.httpStatusCode) { - httpStatusCode = error.httpStatusCode; - } +interface ErrorResponse { + code: number; + message: string; + hint?: string; + stacktrace?: string; +} - if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { - console.error('ERROR RESPONSE'); - console.error(error); - } +export function sendErrorResponse(res: Response, error: Error) { + let httpStatusCode = 500; - const response = { + const response: ErrorResponse = { code: 0, message: 'Unknown error', - hint: '', }; - if (error.name === 'NodeApiError') { - Object.assign(response, error); - } + if (error instanceof ResponseError) { + if (inDevelopment) { + console.error(picocolors.red(error.httpStatusCode), error.message); + } - if (error.errorCode) { - response.code = error.errorCode; - } - if (error.message) { response.message = error.message; + httpStatusCode = error.httpStatusCode; + + if (error.errorCode) { + response.code = error.errorCode; + } + if (error.hint) { + response.hint = error.hint; + } } - if (error.hint) { - response.hint = error.hint; + + if (error instanceof NodeApiError) { + if (inDevelopment) { + console.error(picocolors.red(error.name), error.message); + } + + Object.assign(response, error); } - if (error.stack && process.env.NODE_ENV !== 'production') { - // @ts-ignore - response.stack = error.stack; + + if (error.stack && inDevelopment) { + response.stacktrace = error.stack; } + res.status(httpStatusCode).json(response); } @@ -154,7 +189,9 @@ export function send( sendSuccessResponse(res, data, raw); } catch (error) { if (error instanceof Error) { - ErrorReporter.error(error); + if (!(error instanceof ResponseError) || error.httpStatusCode > 404) { + ErrorReporter.error(error); + } if (isUniqueConstraintError(error)) { error.message = 'There is already an entry with this name'; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 82fbb0079028e..5e21982f931ac 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -157,7 +157,6 @@ import { WaitTracker, WaitTrackerClass } from '@/WaitTracker'; import * as WebhookHelpers from '@/WebhookHelpers'; import * as WebhookServer from '@/WebhookServer'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; -import { ResponseError } from '@/ResponseHelper'; import { toHttpNodeParameters } from '@/CurlConverterHelper'; import { setupErrorMiddleware } from '@/ErrorReporting'; import { getLicense } from '@/License'; @@ -725,7 +724,7 @@ class App { // eslint-disable-next-line consistent-return this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { if (!Db.isInitialized) { - const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503); + const error = new ResponseHelper.ServiceUnavailableError('Database is not ready!'); return ResponseHelper.sendErrorResponse(res, error); } @@ -766,7 +765,7 @@ class App { } catch (err) { ErrorReporter.error(err); LoggerProxy.error('No Database connection!', err); - const error = new ResponseHelper.ResponseError('No Database connection!', undefined, 503); + const error = new ResponseHelper.ServiceUnavailableError('No Database connection!'); return ResponseHelper.sendErrorResponse(res, error); } @@ -869,7 +868,9 @@ class App { const { path, methodName } = req.query; if (!req.query.currentNodeParameters) { - throw new ResponseError('Parameter currentNodeParameters is required.', undefined, 400); + throw new ResponseHelper.BadRequestError( + 'Parameter currentNodeParameters is required.', + ); } const currentNodeParameters = jsonParse( @@ -904,7 +905,7 @@ class App { ); } - throw new ResponseError('Parameter methodName is required.', undefined, 400); + throw new ResponseHelper.BadRequestError('Parameter methodName is required.'); }, ), ); @@ -1076,10 +1077,8 @@ class App { userId: req.user.id, }); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( `Workflow with ID "${workflowId}" could not be found.`, - undefined, - 400, ); } @@ -1103,7 +1102,7 @@ class App { const parameters = toHttpNodeParameters(curlCommand); return ResponseHelper.flattenObject(parameters, 'parameters'); } catch (e) { - throw new ResponseHelper.ResponseError(`Invalid cURL command`, undefined, 400); + throw new ResponseHelper.BadRequestError(`Invalid cURL command`); } }, ), @@ -1178,11 +1177,7 @@ class App { if (!credentialId) { LoggerProxy.error('OAuth1 credential authorization failed due to missing credential ID'); - throw new ResponseHelper.ResponseError( - 'Required credential ID is missing', - undefined, - 400, - ); + throw new ResponseHelper.BadRequestError('Required credential ID is missing'); } const credential = await getCredentialForUser(credentialId, req.user); @@ -1192,18 +1187,14 @@ class App { 'OAuth1 credential authorization failed because the current user does not have the correct permissions', { userId: req.user.id }, ); - throw new ResponseHelper.ResponseError( - RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL, - undefined, - 404, - ); + throw new ResponseHelper.NotFoundError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL); } let encryptionKey: string; try { encryptionKey = await UserSettings.getEncryptionKey(); } catch (error) { - throw new ResponseHelper.ResponseError(error.message, undefined, 500); + throw new ResponseHelper.InternalServerError(error.message); } const mode: WorkflowExecuteMode = 'internal'; @@ -1304,12 +1295,10 @@ class App { const { oauth_verifier, oauth_token, cid: credentialId } = req.query; if (!oauth_verifier || !oauth_token) { - const errorResponse = new ResponseHelper.ResponseError( + const errorResponse = new ResponseHelper.ServiceUnavailableError( `Insufficient parameters for OAuth1 callback. Received following query parameters: ${JSON.stringify( req.query, )}`, - undefined, - 503, ); LoggerProxy.error( 'OAuth1 callback failed because of insufficient parameters received', @@ -1328,10 +1317,8 @@ class App { userId: req.user?.id, credentialId, }); - const errorResponse = new ResponseHelper.ResponseError( + const errorResponse = new ResponseHelper.NotFoundError( RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL, - undefined, - 404, ); return ResponseHelper.sendErrorResponse(res, errorResponse); } @@ -1340,7 +1327,7 @@ class App { try { encryptionKey = await UserSettings.getEncryptionKey(); } catch (error) { - throw new ResponseHelper.ResponseError(error.message, undefined, 500); + throw new ResponseHelper.InternalServerError(error.message); } const mode: WorkflowExecuteMode = 'internal'; @@ -1378,11 +1365,7 @@ class App { userId: req.user?.id, credentialId, }); - const errorResponse = new ResponseHelper.ResponseError( - 'Unable to get access tokens!', - undefined, - 404, - ); + const errorResponse = new ResponseHelper.NotFoundError('Unable to get access tokens!'); return ResponseHelper.sendErrorResponse(res, errorResponse); } @@ -1539,7 +1522,7 @@ class App { const sharedWorkflowIds = await getSharedWorkflowIds(req.user); if (!sharedWorkflowIds.length) { - throw new ResponseHelper.ResponseError('Execution not found', undefined, 404); + throw new ResponseHelper.NotFoundError('Execution not found'); } const execution = await Db.collections.Execution.findOne({ @@ -1550,7 +1533,7 @@ class App { }); if (!execution) { - throw new ResponseHelper.ResponseError('Execution not found', undefined, 404); + throw new ResponseHelper.NotFoundError('Execution not found'); } if (config.getEnv('executions.mode') === 'queue') { diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index 1763dbf8707c3..3142e925b069a 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -67,10 +67,8 @@ export class TestWebhooks { webhookData = this.activeWebhooks!.get(httpMethod, pathElements.join('/'), webhookId); if (webhookData === undefined) { // The requested webhook is not registered - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( `The requested webhook "${httpMethod} ${path}" is not registered.`, - 404, - 404, WEBHOOK_TEST_UNREGISTERED_HINT, ); } @@ -94,10 +92,8 @@ export class TestWebhooks { // TODO: Clean that duplication up one day and improve code generally if (this.testWebhookData[webhookKey] === undefined) { // The requested webhook is not registered - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( `The requested webhook "${httpMethod} ${path}" is not registered.`, - 404, - 404, WEBHOOK_TEST_UNREGISTERED_HINT, ); } @@ -108,7 +104,7 @@ export class TestWebhooks { // get additional data const workflowStartNode = workflow.getNode(webhookData.node); if (workflowStartNode === null) { - throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404); + throw new ResponseHelper.NotFoundError('Could not find node to process webhook.'); } // eslint-disable-next-line no-async-promise-executor @@ -173,10 +169,8 @@ export class TestWebhooks { if (webhookMethods === undefined) { // The requested webhook is not registered - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( `The requested webhook "${path}" is not registered.`, - 404, - 404, WEBHOOK_TEST_UNREGISTERED_HINT, ); } diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 213a5839ebce4..f91de0d19e38c 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -90,7 +90,7 @@ export function getInstanceBaseUrl(): string { // TODO: Enforce at model level export function validatePassword(password?: string): string { if (!password) { - throw new ResponseHelper.ResponseError('Password is mandatory', undefined, 400); + throw new ResponseHelper.BadRequestError('Password is mandatory'); } const hasInvalidLength = @@ -117,7 +117,7 @@ export function validatePassword(password?: string): string { message.push('Password must contain at least 1 uppercase letter.'); } - throw new ResponseHelper.ResponseError(message.join(' '), undefined, 400); + throw new ResponseHelper.BadRequestError(message.join(' ')); } return password; diff --git a/packages/cli/src/UserManagement/routes/auth.ts b/packages/cli/src/UserManagement/routes/auth.ts index 522553df73e93..a9514f20263cf 100644 --- a/packages/cli/src/UserManagement/routes/auth.ts +++ b/packages/cli/src/UserManagement/routes/auth.ts @@ -77,24 +77,20 @@ export function authenticationMethods(this: N8nApp): void { } if (config.get('userManagement.isInstanceOwnerSetUp')) { - throw new ResponseHelper.ResponseError('Not logged in', undefined, 401); + throw new ResponseHelper.AuthError('Not logged in'); } try { user = await Db.collections.User.findOneOrFail({ relations: ['globalRole'] }); } catch (error) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( 'No users found in database - did you wipe the users table? Create at least one user.', - undefined, - 500, ); } if (user.email || user.password) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( 'Invalid database state - user has password set.', - undefined, - 500, ); } diff --git a/packages/cli/src/UserManagement/routes/me.ts b/packages/cli/src/UserManagement/routes/me.ts index 0e6e7ec62ed6c..3e78c67d6bb4f 100644 --- a/packages/cli/src/UserManagement/routes/me.ts +++ b/packages/cli/src/UserManagement/routes/me.ts @@ -39,7 +39,7 @@ export function meNamespace(this: N8nApp): void { userId: req.user.id, payload: req.body, }); - throw new ResponseHelper.ResponseError('Email is mandatory', undefined, 400); + throw new ResponseHelper.BadRequestError('Email is mandatory'); } if (!validator.isEmail(email)) { @@ -47,7 +47,7 @@ export function meNamespace(this: N8nApp): void { userId: req.user.id, invalidEmail: email, }); - throw new ResponseHelper.ResponseError('Invalid email address', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid email address'); } const { email: currentEmail } = req.user; @@ -84,20 +84,16 @@ export function meNamespace(this: N8nApp): void { const { currentPassword, newPassword } = req.body; if (typeof currentPassword !== 'string' || typeof newPassword !== 'string') { - throw new ResponseHelper.ResponseError('Invalid payload.', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid payload.'); } if (!req.user.password) { - throw new ResponseHelper.ResponseError('Requesting user not set up.'); + throw new ResponseHelper.BadRequestError('Requesting user not set up.'); } const isCurrentPwCorrect = await compareHash(currentPassword, req.user.password); if (!isCurrentPwCorrect) { - throw new ResponseHelper.ResponseError( - 'Provided current password is incorrect.', - undefined, - 400, - ); + throw new ResponseHelper.BadRequestError('Provided current password is incorrect.'); } const validPassword = validatePassword(newPassword); @@ -135,11 +131,7 @@ export function meNamespace(this: N8nApp): void { userId: req.user.id, }, ); - throw new ResponseHelper.ResponseError( - 'Personalization answers are mandatory', - undefined, - 400, - ); + throw new ResponseHelper.BadRequestError('Personalization answers are mandatory'); } await Db.collections.User.save({ diff --git a/packages/cli/src/UserManagement/routes/owner.ts b/packages/cli/src/UserManagement/routes/owner.ts index 415ebeb6c3fc9..84c159009fff9 100644 --- a/packages/cli/src/UserManagement/routes/owner.ts +++ b/packages/cli/src/UserManagement/routes/owner.ts @@ -31,7 +31,7 @@ export function ownerNamespace(this: N8nApp): void { userId, }, ); - throw new ResponseHelper.ResponseError('Invalid request', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid request'); } if (!email || !validator.isEmail(email)) { @@ -39,7 +39,7 @@ export function ownerNamespace(this: N8nApp): void { userId, invalidEmail: email, }); - throw new ResponseHelper.ResponseError('Invalid email address', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid email address'); } const validPassword = validatePassword(password); @@ -49,11 +49,7 @@ export function ownerNamespace(this: N8nApp): void { 'Request to claim instance ownership failed because of missing first name or last name in payload', { userId, payload: req.body }, ); - throw new ResponseHelper.ResponseError( - 'First and last names are mandatory', - undefined, - 400, - ); + throw new ResponseHelper.BadRequestError('First and last names are mandatory'); } let owner = await Db.collections.User.findOne(userId, { @@ -67,7 +63,7 @@ export function ownerNamespace(this: N8nApp): void { userId, }, ); - throw new ResponseHelper.ResponseError('Invalid request', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid request'); } owner = Object.assign(owner, { diff --git a/packages/cli/src/UserManagement/routes/passwordReset.ts b/packages/cli/src/UserManagement/routes/passwordReset.ts index 48011d7a11928..98a5c3eea25a6 100644 --- a/packages/cli/src/UserManagement/routes/passwordReset.ts +++ b/packages/cli/src/UserManagement/routes/passwordReset.ts @@ -28,10 +28,8 @@ export function passwordResetNamespace(this: N8nApp): void { ResponseHelper.send(async (req: PasswordResetRequest.Email) => { if (config.getEnv('userManagement.emails.mode') === '') { Logger.debug('Request to send password reset email failed because emailing was not set up'); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( 'Email sending must be set up in order to request a password reset email', - undefined, - 500, ); } @@ -42,7 +40,7 @@ export function passwordResetNamespace(this: N8nApp): void { 'Request to send password reset email failed because of missing email in payload', { payload: req.body }, ); - throw new ResponseHelper.ResponseError('Email is mandatory', undefined, 400); + throw new ResponseHelper.BadRequestError('Email is mandatory'); } if (!validator.isEmail(email)) { @@ -50,7 +48,7 @@ export function passwordResetNamespace(this: N8nApp): void { 'Request to send password reset email failed because of invalid email in payload', { invalidEmail: email }, ); - throw new ResponseHelper.ResponseError('Invalid email address', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid email address'); } // User should just be able to reset password if one is already present @@ -93,10 +91,8 @@ export function passwordResetNamespace(this: N8nApp): void { public_api: false, }); if (error instanceof Error) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( `Please contact your administrator: ${error.message}`, - undefined, - 500, ); } } @@ -131,7 +127,7 @@ export function passwordResetNamespace(this: N8nApp): void { queryString: req.query, }, ); - throw new ResponseHelper.ResponseError('', undefined, 400); + throw new ResponseHelper.BadRequestError(''); } // Timestamp is saved in seconds @@ -151,7 +147,7 @@ export function passwordResetNamespace(this: N8nApp): void { resetPasswordToken, }, ); - throw new ResponseHelper.ResponseError('', undefined, 404); + throw new ResponseHelper.NotFoundError(''); } Logger.info('Reset-password token resolved successfully', { userId: id }); @@ -178,10 +174,8 @@ export function passwordResetNamespace(this: N8nApp): void { payload: req.body, }, ); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( 'Missing user ID or password or reset password token', - undefined, - 400, ); } @@ -204,7 +198,7 @@ export function passwordResetNamespace(this: N8nApp): void { resetPasswordToken, }, ); - throw new ResponseHelper.ResponseError('', undefined, 404); + throw new ResponseHelper.NotFoundError(''); } await Db.collections.User.update(userId, { diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index 4e702c06fe777..c8e56dd41b4e4 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -37,10 +37,8 @@ export function usersNamespace(this: N8nApp): void { Logger.debug( 'Request to send email invite(s) to user(s) failed because emailing was not set up', ); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( 'Email sending must be set up in order to request a password reset email', - undefined, - 500, ); } @@ -49,10 +47,8 @@ export function usersNamespace(this: N8nApp): void { mailer = await UserManagementMailer.getInstance(); } catch (error) { if (error instanceof Error) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( `There is a problem with your SMTP setup! ${error.message}`, - undefined, - 500, ); } } @@ -62,17 +58,15 @@ export function usersNamespace(this: N8nApp): void { Logger.debug( 'Request to send email invite(s) to user(s) failed because user management is disabled', ); - throw new ResponseHelper.ResponseError('User management is disabled'); + throw new ResponseHelper.BadRequestError('User management is disabled'); } if (!config.getEnv('userManagement.isInstanceOwnerSetUp')) { Logger.debug( 'Request to send email invite(s) to user(s) failed because the owner account is not set up', ); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( 'You must set up your own account before inviting others', - undefined, - 400, ); } @@ -83,7 +77,7 @@ export function usersNamespace(this: N8nApp): void { payload: req.body, }, ); - throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid payload'); } if (!req.body.length) return []; @@ -92,19 +86,15 @@ export function usersNamespace(this: N8nApp): void { // Validate payload req.body.forEach((invite) => { if (typeof invite !== 'object' || !invite.email) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( 'Request to send email invite(s) to user(s) failed because the payload is not an array shaped Array<{ email: string }>', - undefined, - 400, ); } if (!validator.isEmail(invite.email)) { Logger.debug('Invalid email in payload', { invalidEmail: invite.email }); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( `Request to send email invite(s) to user(s) failed because of an invalid email address: ${invite.email}`, - undefined, - 400, ); } createUsers[invite.email.toLowerCase()] = null; @@ -116,10 +106,8 @@ export function usersNamespace(this: N8nApp): void { Logger.error( 'Request to send email invite(s) to user(s) failed because no global member role was found in database', ); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( 'Members role not found in database - inconsistent state', - undefined, - 500, ); } @@ -163,7 +151,7 @@ export function usersNamespace(this: N8nApp): void { } catch (error) { ErrorReporter.error(error); Logger.error('Failed to create user shells', { userShells: createUsers }); - throw new ResponseHelper.ResponseError('An error occurred during user creation'); + throw new ResponseHelper.InternalServerError('An error occurred during user creation'); } Logger.info('Created user shell(s) successfully', { userId: req.user.id }); @@ -245,7 +233,7 @@ export function usersNamespace(this: N8nApp): void { 'Request to resolve signup token failed because of missing user IDs in query string', { inviterId, inviteeId }, ); - throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid payload'); } // Postgres validates UUID format @@ -254,7 +242,7 @@ export function usersNamespace(this: N8nApp): void { Logger.debug('Request to resolve signup token failed because of invalid user ID', { userId, }); - throw new ResponseHelper.ResponseError('Invalid userId', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid userId'); } } @@ -265,7 +253,7 @@ export function usersNamespace(this: N8nApp): void { 'Request to resolve signup token failed because the ID of the inviter and/or the ID of the invitee were not found in database', { inviterId, inviteeId }, ); - throw new ResponseHelper.ResponseError('Invalid invite URL', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid invite URL'); } const invitee = users.find((user) => user.id === inviteeId); @@ -275,10 +263,8 @@ export function usersNamespace(this: N8nApp): void { inviterId, inviteeId, }); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( 'The invitation was likely either deleted or already claimed', - undefined, - 400, ); } @@ -291,7 +277,7 @@ export function usersNamespace(this: N8nApp): void { inviterId: inviter?.id, }, ); - throw new ResponseHelper.ResponseError('Invalid request', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid request'); } void InternalHooksManager.getInstance().onUserInviteEmailClick({ @@ -321,7 +307,7 @@ export function usersNamespace(this: N8nApp): void { 'Request to fill out a user shell failed because of missing properties in payload', { payload: req.body }, ); - throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid payload'); } const validPassword = validatePassword(password); @@ -339,7 +325,7 @@ export function usersNamespace(this: N8nApp): void { inviteeId, }, ); - throw new ResponseHelper.ResponseError('Invalid payload or URL', undefined, 400); + throw new ResponseHelper.BadRequestError('Invalid payload or URL'); } const invitee = users.find((user) => user.id === inviteeId) as User; @@ -349,11 +335,7 @@ export function usersNamespace(this: N8nApp): void { 'Request to fill out a user shell failed because the invite had already been accepted', { inviteeId }, ); - throw new ResponseHelper.ResponseError( - 'This invite has been accepted already', - undefined, - 400, - ); + throw new ResponseHelper.BadRequestError('This invite has been accepted already'); } invitee.firstName = firstName; @@ -398,16 +380,14 @@ export function usersNamespace(this: N8nApp): void { 'Request to delete a user failed because it attempted to delete the requesting user', { userId: req.user.id }, ); - throw new ResponseHelper.ResponseError('Cannot delete your own user', undefined, 400); + throw new ResponseHelper.BadRequestError('Cannot delete your own user'); } const { transferId } = req.query; if (transferId === idToDelete) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( 'Request to delete a user failed because the user to delete and the transferee are the same user', - undefined, - 400, ); } @@ -416,10 +396,8 @@ export function usersNamespace(this: N8nApp): void { }); if (!users.length || (transferId && users.length !== 2)) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( 'Request to delete a user failed because the ID of the user to delete and/or the ID of the transferee were not found in DB', - undefined, - 404, ); } @@ -502,10 +480,8 @@ export function usersNamespace(this: N8nApp): void { if (!isEmailSetUp()) { Logger.error('Request to reinvite a user failed because email sending was not set up'); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( 'Email sending must be set up in order to invite other users', - undefined, - 500, ); } @@ -515,7 +491,7 @@ export function usersNamespace(this: N8nApp): void { Logger.debug( 'Request to reinvite a user failed because the ID of the reinvitee was not found in database', ); - throw new ResponseHelper.ResponseError('Could not find user', undefined, 404); + throw new ResponseHelper.NotFoundError('Could not find user'); } if (reinvitee.password) { @@ -523,11 +499,7 @@ export function usersNamespace(this: N8nApp): void { 'Request to reinvite a user failed because the invite had already been accepted', { userId: reinvitee.id }, ); - throw new ResponseHelper.ResponseError( - 'User has already accepted the invite', - undefined, - 400, - ); + throw new ResponseHelper.BadRequestError('User has already accepted the invite'); } const baseUrl = getInstanceBaseUrl(); @@ -538,7 +510,7 @@ export function usersNamespace(this: N8nApp): void { mailer = await UserManagementMailer.getInstance(); } catch (error) { if (error instanceof Error) { - throw new ResponseHelper.ResponseError(error.message, undefined, 500); + throw new ResponseHelper.InternalServerError(error.message); } } @@ -559,11 +531,7 @@ export function usersNamespace(this: N8nApp): void { inviteAcceptUrl, domain: baseUrl, }); - throw new ResponseHelper.ResponseError( - `Failed to send email to ${reinvitee.email}`, - undefined, - 500, - ); + throw new ResponseHelper.InternalServerError(`Failed to send email to ${reinvitee.email}`); } void InternalHooksManager.getInstance().onUserReinvite({ diff --git a/packages/cli/src/WaitingWebhooks.ts b/packages/cli/src/WaitingWebhooks.ts index e61390232511c..5915cf89879dd 100644 --- a/packages/cli/src/WaitingWebhooks.ts +++ b/packages/cli/src/WaitingWebhooks.ts @@ -44,21 +44,13 @@ export class WaitingWebhooks { const execution = await Db.collections.Execution.findOne(executionId); if (execution === undefined) { - throw new ResponseHelper.ResponseError( - `The execution "${executionId} does not exist.`, - 404, - 404, - ); + throw new ResponseHelper.NotFoundError(`The execution "${executionId} does not exist.`); } const fullExecutionData = ResponseHelper.unflattenExecutionData(execution); if (fullExecutionData.finished || fullExecutionData.data.resultData.error) { - throw new ResponseHelper.ResponseError( - `The execution "${executionId} has finished already.`, - 409, - 409, - ); + throw new ResponseHelper.ConflictError(`The execution "${executionId} has finished already.`); } return this.startExecution(httpMethod, path, fullExecutionData, req, res); @@ -107,7 +99,7 @@ export class WaitingWebhooks { try { workflowOwner = await getWorkflowOwner(workflowData.id!.toString()); } catch (error) { - throw new ResponseHelper.ResponseError('Could not find workflow', undefined, 404); + throw new ResponseHelper.NotFoundError('Could not find workflow'); } const additionalData = await WorkflowExecuteAdditionalData.getBase(workflowOwner.id); @@ -128,13 +120,13 @@ export class WaitingWebhooks { // If no data got found it means that the execution can not be started via a webhook. // Return 404 because we do not want to give any data if the execution exists or not. const errorMessage = `The execution "${executionId}" with webhook suffix path "${path}" is not known.`; - throw new ResponseHelper.ResponseError(errorMessage, 404, 404); + throw new ResponseHelper.NotFoundError(errorMessage); } const workflowStartNode = workflow.getNode(lastNodeExecuted); if (workflowStartNode === null) { - throw new ResponseHelper.ResponseError('Could not find node to process webhook.', 404, 404); + throw new ResponseHelper.NotFoundError('Could not find node to process webhook.'); } const runExecutionData = fullExecutionData.data; diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index f16ebad4ba10d..f1b4d02744bb9 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -152,7 +152,7 @@ export async function executeWebhook( if (nodeType === undefined) { const errorMessage = `The type of the webhook node "${workflowStartNode.name}" is not known`; responseCallback(new Error(errorMessage), {}); - throw new ResponseHelper.ResponseError(errorMessage, 500, 500); + throw new ResponseHelper.InternalServerError(errorMessage); } const additionalKeys: IWorkflowDataProxyAdditionalKeys = { @@ -169,7 +169,7 @@ export async function executeWebhook( try { user = await getWorkflowOwner(workflowData.id.toString()); } catch (error) { - throw new ResponseHelper.ResponseError('Cannot find workflow', undefined, 404); + throw new ResponseHelper.NotFoundError('Cannot find workflow'); } } @@ -212,7 +212,7 @@ export async function executeWebhook( // that something does not resolve properly. const errorMessage = `The response mode '${responseMode}' is not valid!`; responseCallback(new Error(errorMessage), {}); - throw new ResponseHelper.ResponseError(errorMessage, 500, 500); + throw new ResponseHelper.InternalServerError(errorMessage); } // Add the Response and Request so that this data can be accessed in the node @@ -661,7 +661,7 @@ export async function executeWebhook( responseCallback(new Error('There was a problem executing the workflow'), {}); } - throw new ResponseHelper.ResponseError(e.message, 500, 500); + throw new ResponseHelper.InternalServerError(e.message); }); // eslint-disable-next-line consistent-return @@ -671,7 +671,7 @@ export async function executeWebhook( responseCallback(new Error('There was a problem executing the workflow'), {}); } - throw new ResponseHelper.ResponseError(e.message, 500, 500); + throw new ResponseHelper.InternalServerError(e.message); } } diff --git a/packages/cli/src/WebhookServer.ts b/packages/cli/src/WebhookServer.ts index c1a2db47d8035..b8580b59ea306 100644 --- a/packages/cli/src/WebhookServer.ts +++ b/packages/cli/src/WebhookServer.ts @@ -294,7 +294,7 @@ class App { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { if (!Db.isInitialized) { - const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503); + const error = new ResponseHelper.ServiceUnavailableError('Database is not ready!'); return ResponseHelper.sendErrorResponse(res, error); } @@ -318,7 +318,7 @@ class App { await connection.query('SELECT 1'); // eslint-disable-next-line id-denylist } catch (err) { - const error = new ResponseHelper.ResponseError('No Database connection!', undefined, 503); + const error = new ResponseHelper.ServiceUnavailableError('No Database connection!'); return ResponseHelper.sendErrorResponse(res, error); } diff --git a/packages/cli/src/api/executions.api.ts b/packages/cli/src/api/executions.api.ts index 31d9448102fea..0940b5f36292e 100644 --- a/packages/cli/src/api/executions.api.ts +++ b/packages/cli/src/api/executions.api.ts @@ -172,10 +172,8 @@ executionsController.get( userId: req.user.id, filter: req.query.filter, }); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( `Parameter "filter" contained invalid JSON string.`, - 500, - 500, ); } } @@ -363,10 +361,8 @@ executionsController.post( executionId, }, ); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( `The execution with the ID "${executionId}" does not exist.`, - 404, - 404, ); } @@ -485,10 +481,8 @@ executionsController.post( requestFilters = requestFiltersRaw as IGetExecutionsQueryFilter; } } catch (error) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( `Parameter "filter" contained invalid JSON string.`, - 500, - 500, ); } } diff --git a/packages/cli/src/api/nodes.api.ts b/packages/cli/src/api/nodes.api.ts index 8acc5d64a82fe..f1dc562295fa6 100644 --- a/packages/cli/src/api/nodes.api.ts +++ b/packages/cli/src/api/nodes.api.ts @@ -71,7 +71,7 @@ nodesController.post( const { name } = req.body; if (!name) { - throw new ResponseHelper.ResponseError(PACKAGE_NAME_NOT_PROVIDED, undefined, 400); + throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED); } let parsed: CommunityPackages.ParsedPackageName; @@ -79,21 +79,17 @@ nodesController.post( try { parsed = parseNpmPackageName(name); } catch (error) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( error instanceof Error ? error.message : 'Failed to parse package name', - undefined, - 400, ); } if (parsed.packageName === STARTER_TEMPLATE_NAME) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( [ `Package "${parsed.packageName}" is only a template`, 'Please enter an actual package to install', ].join('.'), - undefined, - 400, ); } @@ -101,23 +97,19 @@ nodesController.post( const hasLoaded = hasPackageLoaded(name); if (isInstalled && hasLoaded) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( [ `Package "${parsed.packageName}" is already installed`, 'To update it, click the corresponding button in the UI', ].join('.'), - undefined, - 400, ); } const packageStatus = await checkNpmPackageStatus(name); if (packageStatus.status !== 'OK') { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( `Package "${name}" is banned so it cannot be installed`, - undefined, - 400, ); } @@ -144,7 +136,7 @@ nodesController.post( const clientError = error instanceof Error ? isClientError(error) : false; - throw new ResponseHelper.ResponseError(message, undefined, clientError ? 400 : 500); + throw new ResponseHelper[clientError ? 'BadRequestError' : 'InternalServerError'](message); } if (!hasLoaded) removePackageFromMissingList(name); @@ -228,7 +220,7 @@ nodesController.delete( const { name } = req.query; if (!name) { - throw new ResponseHelper.ResponseError(PACKAGE_NAME_NOT_PROVIDED, undefined, 400); + throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED); } try { @@ -236,13 +228,13 @@ nodesController.delete( } catch (error) { const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON; - throw new ResponseHelper.ResponseError(message, undefined, 400); + throw new ResponseHelper.BadRequestError(message); } const installedPackage = await findInstalledPackage(name); if (!installedPackage) { - throw new ResponseHelper.ResponseError(PACKAGE_NOT_INSTALLED, undefined, 400); + throw new ResponseHelper.BadRequestError(PACKAGE_NOT_INSTALLED); } try { @@ -253,7 +245,7 @@ nodesController.delete( error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON, ].join(':'); - throw new ResponseHelper.ResponseError(message, undefined, 500); + throw new ResponseHelper.InternalServerError(message); } const pushInstance = Push.getInstance(); @@ -288,13 +280,13 @@ nodesController.patch( const { name } = req.body; if (!name) { - throw new ResponseHelper.ResponseError(PACKAGE_NAME_NOT_PROVIDED, undefined, 400); + throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED); } const previouslyInstalledPackage = await findInstalledPackage(name); if (!previouslyInstalledPackage) { - throw new ResponseHelper.ResponseError(PACKAGE_NOT_INSTALLED, undefined, 400); + throw new ResponseHelper.BadRequestError(PACKAGE_NOT_INSTALLED); } try { @@ -345,7 +337,7 @@ nodesController.patch( error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON, ].join(':'); - throw new ResponseHelper.ResponseError(message, undefined, 500); + throw new ResponseHelper.InternalServerError(message); } }), ); diff --git a/packages/cli/src/api/tags.api.ts b/packages/cli/src/api/tags.api.ts index 1008bf4a7077a..9d6329b1d6da5 100644 --- a/packages/cli/src/api/tags.api.ts +++ b/packages/cli/src/api/tags.api.ts @@ -21,13 +21,18 @@ export const externalHooks: IExternalHooksClass = ExternalHooks(); export const tagsController = express.Router(); +const workflowsEnabledMiddleware: express.RequestHandler = (req, res, next) => { + if (config.getEnv('workflowTagsDisabled')) { + throw new ResponseHelper.BadRequestError('Workflow tags are disabled'); + } + next(); +}; + // Retrieves all tags, with or without usage count tagsController.get( '/', + workflowsEnabledMiddleware, ResponseHelper.send(async (req: express.Request): Promise => { - if (config.getEnv('workflowTagsDisabled')) { - throw new ResponseHelper.ResponseError('Workflow tags are disabled'); - } if (req.query.withUsageCount === 'true') { const tablePrefix = config.getEnv('database.tablePrefix'); return TagHelpers.getTagsWithCountDb(tablePrefix); @@ -40,10 +45,8 @@ tagsController.get( // Creates a tag tagsController.post( '/', + workflowsEnabledMiddleware, ResponseHelper.send(async (req: express.Request): Promise => { - if (config.getEnv('workflowTagsDisabled')) { - throw new ResponseHelper.ResponseError('Workflow tags are disabled'); - } const newTag = new TagEntity(); newTag.name = req.body.name.trim(); @@ -61,11 +64,8 @@ tagsController.post( // Updates a tag tagsController.patch( '/:id', + workflowsEnabledMiddleware, ResponseHelper.send(async (req: express.Request): Promise => { - if (config.getEnv('workflowTagsDisabled')) { - throw new ResponseHelper.ResponseError('Workflow tags are disabled'); - } - const { name } = req.body; const { id } = req.params; @@ -87,18 +87,14 @@ tagsController.patch( tagsController.delete( '/:id', + workflowsEnabledMiddleware, ResponseHelper.send(async (req: TagsRequest.Delete): Promise => { - if (config.getEnv('workflowTagsDisabled')) { - throw new ResponseHelper.ResponseError('Workflow tags are disabled'); - } if ( config.getEnv('userManagement.isInstanceOwnerSetUp') === true && req.user.globalRole.name !== 'owner' ) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.UnauthorizedError( 'You are not allowed to perform this action', - undefined, - 403, 'Only owners can remove tags', ); } diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index d58420c0c1b99..7f3bd6e7ac18d 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -391,11 +391,7 @@ export class Worker extends Command { await connection.query('SELECT 1'); } catch (e) { LoggerProxy.error('No Database connection!', e); - const error = new ResponseHelper.ResponseError( - 'No Database connection!', - undefined, - 503, - ); + const error = new ResponseHelper.ServiceUnavailableError('No Database connection!'); return ResponseHelper.sendErrorResponse(res, error); } @@ -406,11 +402,7 @@ export class Worker extends Command { await Worker.jobQueue.client.ping(); } catch (e) { LoggerProxy.error('No Redis connection!', e); - const error = new ResponseHelper.ResponseError( - 'No Redis connection!', - undefined, - 503, - ); + const error = new ResponseHelper.ServiceUnavailableError('No Redis connection!'); return ResponseHelper.sendErrorResponse(res, error); } diff --git a/packages/cli/src/credentials/credentials.controller.ee.ts b/packages/cli/src/credentials/credentials.controller.ee.ts index 4c744d226d2ef..0ff7e37f82b68 100644 --- a/packages/cli/src/credentials/credentials.controller.ee.ts +++ b/packages/cli/src/credentials/credentials.controller.ee.ts @@ -54,7 +54,7 @@ EECredentialsController.get( const includeDecryptedData = req.query.includeData === 'true'; if (Number.isNaN(Number(credentialId))) { - throw new ResponseHelper.ResponseError(`Credential ID must be a number.`, undefined, 400); + throw new ResponseHelper.BadRequestError(`Credential ID must be a number.`); } let credential = (await EECredentials.get( @@ -63,17 +63,15 @@ EECredentialsController.get( )) as CredentialsEntity & CredentialWithSharings; if (!credential) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( 'Could not load the credential. If you think this is an error, ask the owner to share it with you again', - undefined, - 404, ); } const userSharing = credential.shared?.find((shared) => shared.user.id === req.user.id); if (!userSharing && req.user.globalRole.name !== 'owner') { - throw new ResponseHelper.ResponseError(`Forbidden.`, undefined, 403); + throw new ResponseHelper.UnauthorizedError(`Forbidden.`); } credential = EECredentials.addOwnerAndSharings(credential); @@ -117,7 +115,7 @@ EECredentialsController.post( if (!ownsCredential) { const sharing = await EECredentials.getSharing(req.user, credentials.id); if (!sharing) { - throw new ResponseHelper.ResponseError(`Forbidden`, undefined, 403); + throw new ResponseHelper.UnauthorizedError(`Forbidden`); } const decryptedData = await EECredentials.decrypt(encryptionKey, sharing.credentials); @@ -144,13 +142,13 @@ EECredentialsController.put( !Array.isArray(shareWithIds) || !shareWithIds.every((userId) => typeof userId === 'string') ) { - throw new ResponseHelper.ResponseError('Bad request', undefined, 400); + throw new ResponseHelper.BadRequestError('Bad request'); } const { ownsCredential, credential } = await EECredentials.isOwned(req.user, credentialId); if (!ownsCredential || !credential) { - throw new ResponseHelper.ResponseError('Forbidden', undefined, 403); + throw new ResponseHelper.UnauthorizedError('Forbidden'); } let amountRemoved: number | null = null; diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index 5d069d175b942..5c66e0c467525 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -75,16 +75,14 @@ credentialsController.get( const includeDecryptedData = req.query.includeData === 'true'; if (Number.isNaN(Number(credentialId))) { - throw new ResponseHelper.ResponseError(`Credential ID must be a number.`, undefined, 400); + throw new ResponseHelper.BadRequestError(`Credential ID must be a number.`); } const sharing = await CredentialsService.getSharing(req.user, credentialId, ['credentials']); if (!sharing) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( `Credential with ID "${credentialId}" could not be found.`, - undefined, - 404, ); } @@ -159,10 +157,8 @@ credentialsController.patch( credentialId, userId: req.user.id, }); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( 'Credential to be updated not found. You can only update credentials owned by you', - undefined, - 404, ); } @@ -183,10 +179,8 @@ credentialsController.patch( const responseData = await CredentialsService.update(credentialId, newCredentialData); if (responseData === undefined) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( `Credential ID "${credentialId}" could not be found to be updated.`, - undefined, - 404, ); } @@ -217,10 +211,8 @@ credentialsController.delete( credentialId, userId: req.user.id, }); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( 'Credential to be deleted not found. You can only removed credentials owned by you', - undefined, - 404, ); } diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index 7833b87f5b2cc..1c90a64419c11 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -206,11 +206,7 @@ export class CredentialsService { try { return await UserSettings.getEncryptionKey(); } catch (error) { - throw new ResponseHelper.ResponseError( - RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY, - undefined, - 500, - ); + throw new ResponseHelper.InternalServerError(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY); } } diff --git a/packages/cli/src/credentials/oauth2Credential.api.ts b/packages/cli/src/credentials/oauth2Credential.api.ts index 3273bf1ce9ac3..e3fe834d4327a 100644 --- a/packages/cli/src/credentials/oauth2Credential.api.ts +++ b/packages/cli/src/credentials/oauth2Credential.api.ts @@ -58,7 +58,7 @@ oauth2CredentialController.get( const { id: credentialId } = req.query; if (!credentialId) { - throw new ResponseHelper.ResponseError('Required credential ID is missing', undefined, 400); + throw new ResponseHelper.BadRequestError('Required credential ID is missing'); } const credential = await getCredentialForUser(credentialId, req.user); @@ -68,14 +68,14 @@ oauth2CredentialController.get( userId: req.user.id, credentialId, }); - throw new ResponseHelper.ResponseError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL, undefined, 404); + throw new ResponseHelper.NotFoundError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL); } let encryptionKey: string; try { encryptionKey = await UserSettings.getEncryptionKey(); } catch (error) { - throw new ResponseHelper.ResponseError((error as Error).message, undefined, 500); + throw new ResponseHelper.InternalServerError((error as Error).message); } const mode: WorkflowExecuteMode = 'internal'; @@ -173,12 +173,10 @@ oauth2CredentialController.get( const { code, state: stateEncoded } = req.query; if (!code || !stateEncoded) { - const errorResponse = new ResponseHelper.ResponseError( + const errorResponse = new ResponseHelper.ServiceUnavailableError( `Insufficient parameters for OAuth2 callback. Received following query parameters: ${JSON.stringify( req.query, )}`, - undefined, - 503, ); return ResponseHelper.sendErrorResponse(res, errorResponse); } @@ -190,10 +188,8 @@ oauth2CredentialController.get( token: string; }; } catch (error) { - const errorResponse = new ResponseHelper.ResponseError( + const errorResponse = new ResponseHelper.ServiceUnavailableError( 'Invalid state format returned', - undefined, - 503, ); return ResponseHelper.sendErrorResponse(res, errorResponse); } @@ -205,10 +201,8 @@ oauth2CredentialController.get( userId: req.user?.id, credentialId: state.cid, }); - const errorResponse = new ResponseHelper.ResponseError( + const errorResponse = new ResponseHelper.NotFoundError( RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL, - undefined, - 404, ); return ResponseHelper.sendErrorResponse(res, errorResponse); } @@ -217,11 +211,7 @@ oauth2CredentialController.get( try { encryptionKey = await UserSettings.getEncryptionKey(); } catch (error) { - throw new ResponseHelper.ResponseError( - (error as IDataObject).message as string, - undefined, - 500, - ); + throw new ResponseHelper.InternalServerError((error as IDataObject).message as string); } const mode: WorkflowExecuteMode = 'internal'; @@ -250,10 +240,8 @@ oauth2CredentialController.get( userId: req.user?.id, credentialId: state.cid, }); - const errorResponse = new ResponseHelper.ResponseError( + const errorResponse = new ResponseHelper.NotFoundError( 'The OAuth2 callback state is invalid!', - undefined, - 404, ); return ResponseHelper.sendErrorResponse(res, errorResponse); } @@ -299,11 +287,7 @@ oauth2CredentialController.get( userId: req.user?.id, credentialId: state.cid, }); - const errorResponse = new ResponseHelper.ResponseError( - 'Unable to get access tokens!', - undefined, - 404, - ); + const errorResponse = new ResponseHelper.NotFoundError('Unable to get access tokens!'); return ResponseHelper.sendErrorResponse(res, errorResponse); } diff --git a/packages/cli/src/workflows/workflows.controller.ee.ts b/packages/cli/src/workflows/workflows.controller.ee.ts index 8d211af95a919..b24d1b7c11e7e 100644 --- a/packages/cli/src/workflows/workflows.controller.ee.ts +++ b/packages/cli/src/workflows/workflows.controller.ee.ts @@ -46,13 +46,13 @@ EEWorkflowController.put( !Array.isArray(shareWithIds) || !shareWithIds.every((userId) => typeof userId === 'string') ) { - throw new ResponseHelper.ResponseError('Bad request', undefined, 400); + throw new ResponseHelper.BadRequestError('Bad request'); } const { ownsWorkflow, workflow } = await EEWorkflows.isOwned(req.user, workflowId); if (!ownsWorkflow || !workflow) { - throw new ResponseHelper.ResponseError('Forbidden', undefined, 403); + throw new ResponseHelper.UnauthorizedError('Forbidden'); } let newShareeIds: string[] = []; @@ -86,20 +86,14 @@ EEWorkflowController.get( ); if (!workflow) { - throw new ResponseHelper.ResponseError( - `Workflow with ID "${workflowId}" does not exist`, - undefined, - 404, - ); + throw new ResponseHelper.NotFoundError(`Workflow with ID "${workflowId}" does not exist`); } const userSharing = workflow.shared?.find((shared) => shared.user.id === req.user.id); if (!userSharing && req.user.globalRole.name !== 'owner') { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.UnauthorizedError( 'It looks like you cannot access this workflow. Ask the owner to share it with you.', - undefined, - 403, ); } @@ -143,10 +137,8 @@ EEWorkflowController.post( try { EEWorkflows.validateCredentialPermissionsToUser(newWorkflow, allCredentials); } catch (error) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( 'The workflow you are trying to save contains credentials that are not shared with you', - undefined, - 400, ); } @@ -173,7 +165,7 @@ EEWorkflowController.post( if (!savedWorkflow) { LoggerProxy.error('Failed to create workflow', { userId: req.user.id }); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( 'An error occurred while saving your workflow. Please try again.', ); } diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index cb5418342f6a0..ae4831ac7b181 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -91,7 +91,7 @@ workflowsController.post( if (!savedWorkflow) { LoggerProxy.error('Failed to create workflow', { userId: req.user.id }); - throw new ResponseHelper.ResponseError('Failed to save workflow'); + throw new ResponseHelper.InternalServerError('Failed to save workflow'); } if (tagIds && !config.getEnv('workflowTagsDisabled') && savedWorkflow.tags) { @@ -152,13 +152,11 @@ workflowsController.get( `/from-url`, ResponseHelper.send(async (req: express.Request): Promise => { if (req.query.url === undefined) { - throw new ResponseHelper.ResponseError(`The parameter "url" is missing!`, undefined, 400); + throw new ResponseHelper.BadRequestError(`The parameter "url" is missing!`); } if (!/^http[s]?:\/\/.*\.json$/i.exec(req.query.url as string)) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( `The parameter "url" is not valid! It does not seem to be a URL pointing to a n8n workflow JSON file.`, - undefined, - 400, ); } let workflowData: IWorkflowResponse | undefined; @@ -166,11 +164,7 @@ workflowsController.get( const { data } = await axios.get(req.query.url as string); workflowData = data; } catch (error) { - throw new ResponseHelper.ResponseError( - `The URL does not point to valid JSON file!`, - undefined, - 400, - ); + throw new ResponseHelper.BadRequestError(`The URL does not point to valid JSON file!`); } // Do a very basic check if it is really a n8n-workflow-json @@ -182,10 +176,8 @@ workflowsController.get( typeof workflowData.connections !== 'object' || Array.isArray(workflowData.connections) ) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( `The data in the file does not seem to be a n8n workflow JSON file!`, - undefined, - 400, ); } @@ -222,10 +214,8 @@ workflowsController.get( workflowId, userId: req.user.id, }); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( 'Could not load the workflow - you can only access workflows owned by you', - undefined, - 404, ); } @@ -297,10 +287,8 @@ workflowsController.delete( workflowId, userId: req.user.id, }); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( 'Could not delete the workflow - you can only remove workflows owned by you', - undefined, - 400, ); } diff --git a/packages/cli/src/workflows/workflows.services.ee.ts b/packages/cli/src/workflows/workflows.services.ee.ts index 7b0a3afac7fda..ae15435943f4f 100644 --- a/packages/cli/src/workflows/workflows.services.ee.ts +++ b/packages/cli/src/workflows/workflows.services.ee.ts @@ -168,7 +168,7 @@ export class EEWorkflowsService extends WorkflowsService { const previousVersion = await EEWorkflowsService.get({ id: parseInt(workflowId, 10) }); if (!previousVersion) { - throw new ResponseHelper.ResponseError('Workflow not found', undefined, 404); + throw new ResponseHelper.NotFoundError('Workflow not found'); } const allCredentials = await EECredentials.getAll(user); @@ -180,10 +180,8 @@ export class EEWorkflowsService extends WorkflowsService { allCredentials, ); } catch (error) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( 'Invalid workflow credentials - make sure you have access to all credentials and try again.', - undefined, - 400, ); } } diff --git a/packages/cli/src/workflows/workflows.services.ts b/packages/cli/src/workflows/workflows.services.ts index d497ddecb6773..9b4df6e17d9ff 100644 --- a/packages/cli/src/workflows/workflows.services.ts +++ b/packages/cli/src/workflows/workflows.services.ts @@ -124,10 +124,8 @@ export class WorkflowsService { userId: user.id, filter, }); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.InternalServerError( `Parameter "filter" contained invalid JSON string.`, - 500, - 500, ); } } @@ -196,18 +194,14 @@ export class WorkflowsService { workflowId, userId: user.id, }); - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.NotFoundError( 'You do not have permission to update this workflow. Ask the owner to share it with you.', - undefined, - 404, ); } if (!forceSave && workflow.hash !== '' && workflow.hash !== shared.workflow.hash) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( 'We are sorry, but the workflow has been changed in the meantime. Please reload the workflow and try again.', - undefined, - 400, ); } @@ -290,10 +284,8 @@ export class WorkflowsService { const updatedWorkflow = await Db.collections.Workflow.findOne(workflowId, options); if (updatedWorkflow === undefined) { - throw new ResponseHelper.ResponseError( + throw new ResponseHelper.BadRequestError( `Workflow with ID "${workflowId}" could not be found to be updated.`, - undefined, - 400, ); } diff --git a/packages/workflow/src/ErrorReporterProxy.ts b/packages/workflow/src/ErrorReporterProxy.ts index 480add7ec6896..2dd3dfce6ab20 100644 --- a/packages/workflow/src/ErrorReporterProxy.ts +++ b/packages/workflow/src/ErrorReporterProxy.ts @@ -11,10 +11,9 @@ interface ErrorReporter { report: (error: Error | string, options?: ReportingOptions) => void; } -const isProduction = process.env.NODE_ENV === 'production'; - const instance: ErrorReporter = { - report: (error, options) => isProduction && Logger.error('ERROR', { error, options }), + report: (error) => + error instanceof Error && Logger.error(`${error.constructor.name}: ${error.message}`), }; export function init(errorReporter: ErrorReporter) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82a4ac5673258..a18082a6f82b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,6 +183,7 @@ importers: passport-cookie: ^1.0.9 passport-jwt: ^4.0.0 pg: ^8.3.0 + picocolors: ^1.0.0 posthog-node: ^1.3.0 prom-client: ^13.1.0 psl: ^1.8.0 @@ -263,6 +264,7 @@ importers: passport-cookie: 1.0.9 passport-jwt: 4.0.0 pg: 8.8.0 + picocolors: 1.0.0 posthog-node: 1.3.0 prom-client: 13.2.0 psl: 1.9.0