feat(editor): Improve errors in output panel (#8644)

Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
Giulio Andreini
2024-03-07 17:08:01 +01:00
committed by GitHub
parent 6e2aa405fc
commit 5301323906
38 changed files with 772 additions and 287 deletions

View File

@@ -1,4 +1,9 @@
import { NodeOperationError, type IExecuteFunctions, type INodeExecutionData } from 'n8n-workflow';
import {
NodeOperationError,
type IExecuteFunctions,
type INodeExecutionData,
NodeApiError,
} from 'n8n-workflow';
import * as assistant from './assistant';
import * as audio from './audio';
@@ -54,6 +59,15 @@ export async function router(this: IExecuteFunctions) {
returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
continue;
}
if (error instanceof NodeApiError) {
error.context = {
itemIndex: i,
};
throw error;
}
throw new NodeOperationError(this.getNode(), error, {
itemIndex: i,
description: error.description,

View File

@@ -98,6 +98,7 @@ export class FrontendService {
timezone: config.getEnv('generic.timezone'),
urlBaseWebhook: this.urlService.getWebhookBaseUrl(),
urlBaseEditor: instanceBaseUrl,
binaryDataMode: config.getEnv('binaryDataManager.mode'),
versionCli: '',
releaseChannel: config.getEnv('generic.releaseChannel'),
oauthCallbackUrls: {
@@ -308,6 +309,8 @@ export class FrontendService {
this.settings.executionMode = config.getEnv('executions.mode');
this.settings.binaryDataMode = config.getEnv('binaryDataManager.mode');
return this.settings;
}

View File

@@ -1106,6 +1106,7 @@ export interface RootState {
urlBaseEditor: string;
instanceId: string;
isNpmAvailable: boolean;
binaryDataMode: string;
}
export interface NodeMetadataMap {
@@ -1154,6 +1155,7 @@ export interface IRootState {
nodeMetadata: NodeMetadataMap;
isNpmAvailable: boolean;
subworkflowExecutionError: Error | null;
binaryDataMode: string;
}
export interface CommunityPackageMap {

View File

@@ -92,6 +92,7 @@ const defaultSettings: IN8nUISettings = {
banners: {
dismissed: [],
},
binaryDataMode: 'default',
};
export function routesForSettings(server: Server) {

View File

@@ -144,4 +144,5 @@ export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
saveManualExecutions: false,
binaryDataMode: 'default',
};

View File

@@ -1,126 +1,216 @@
<template>
<div>
<div class="error-header">
<div class="error-message" v-text="getErrorMessage()" />
<div v-if="error.description" class="error-description" v-html="getErrorDescription()"></div>
<div class="node-error-view">
<div class="node-error-view__header">
<div class="node-error-view__header-message" v-text="getErrorMessage()" />
<div
class="node-error-view__header-description"
v-if="error.description"
v-html="getErrorDescription()"
></div>
</div>
<details>
<summary class="error-details__summary">
<font-awesome-icon class="error-details__icon" icon="angle-right" />
{{ $locale.baseText('nodeErrorView.details') }}
</summary>
<div class="error-details__content">
<div v-if="error.context && error.context.causeDetailed">
<el-card class="box-card" shadow="never">
<div>
{{ error.context.causeDetailed }}
</div>
</el-card>
</div>
<div v-if="error.timestamp">
<el-card class="box-card" shadow="never">
<template #header>
<div class="clearfix box-card__title">
<span>{{ $locale.baseText('nodeErrorView.time') }}</span>
</div>
</template>
<div>
{{ new Date(error.timestamp).toLocaleString() }}
</div>
</el-card>
</div>
<div
v-if="error.context && error.context.itemIndex !== undefined"
class="el-card box-card is-never-shadow el-card__body"
<div class="node-error-view__info">
<div class="node-error-view__info-header">
<p class="node-error-view__info-title">
{{ $locale.baseText('nodeErrorView.details.title') }}
</p>
<n8n-tooltip
class="item"
:content="$locale.baseText('nodeErrorView.copyToClipboard.tooltip')"
placement="left"
>
<span class="error-details__summary"
>{{ $locale.baseText('nodeErrorView.itemIndex') }}:</span
>
{{ error.context.itemIndex }}
<span v-if="error.context.runIndex">
|
<span class="error-details__summary"
>{{ $locale.baseText('nodeErrorView.itemIndex') }}:</span
>
{{ error.context.runIndex }}
</span>
<span v-if="error.context.parameter">
|
<span class="error-details__summary"
>{{ $locale.baseText('nodeErrorView.inParameter') }}:</span
>
{{ parameterDisplayName(error.context.parameter) }}
</span>
</div>
<div v-if="error.httpCode">
<el-card class="box-card" shadow="never">
<template #header>
<div class="clearfix box-card__title">
<span>{{ $locale.baseText('nodeErrorView.httpCode') }}</span>
</div>
</template>
<div>
{{ error.httpCode }}
</div>
</el-card>
</div>
<div v-if="error.cause">
<el-card class="box-card" shadow="never">
<template #header>
<div class="clearfix box-card__title">
<span>{{ $locale.baseText('nodeErrorView.cause') }}</span>
<br />
<span class="box-card__subtitle">{{
$locale.baseText('nodeErrorView.dataBelowMayContain')
}}</span>
</div>
</template>
<div>
<div v-if="displayCause" class="copy-button">
<n8n-icon-button
:title="$locale.baseText('nodeErrorView.copyToClipboard')"
icon="copy"
@click="copyCause"
/>
</div>
<VueJsonPretty
v-if="displayCause"
:data="error.cause"
:deep="3"
:show-length="true"
selectable-type="single"
path="error"
class="json-data"
/>
<span v-else>
<font-awesome-icon icon="info-circle" />{{
$locale.baseText('nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed')
}}
</span>
</div>
</el-card>
</div>
<div v-if="error.stack">
<el-card class="box-card" shadow="never">
<template #header>
<div class="clearfix box-card__title">
<span>{{ $locale.baseText('nodeErrorView.stack') }}</span>
</div>
</template>
<div>
<pre><code>{{error.stack}}</code></pre>
</div>
</el-card>
</div>
<div class="copy-button">
<n8n-icon-button
icon="copy"
type="secondary"
size="mini"
text="true"
transparent-background="transparent"
@click="copyErrorDetails"
/>
</div>
</n8n-tooltip>
</div>
</details>
<div class="node-error-view__info-content">
<details
class="node-error-view__details"
v-if="error.httpCode || prepareRawMessages.length || error?.context?.data || error.extra"
>
<summary class="node-error-view__details-summary">
<font-awesome-icon class="node-error-view__details-icon" icon="angle-right" />
{{
$locale.baseText('nodeErrorView.details.from', {
interpolate: { node: getNodeDefaultName(error?.node) as string },
})
}}
</summary>
<div class="node-error-view__details-content">
<div class="node-error-view__details-row" v-if="error.httpCode">
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.errorCode') }}
</p>
<p class="node-error-view__details-value">
<code>{{ error.httpCode }}</code>
</p>
</div>
<div class="node-error-view__details-row" v-if="prepareRawMessages.length">
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.details.rawMessages') }}
</p>
<div class="node-error-view__details-value">
<div v-for="(msg, index) in prepareRawMessages" :key="index">
<pre><code>{{ msg }}</code></pre>
</div>
</div>
</div>
<div class="node-error-view__details-row" v-if="error?.context?.data">
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.details.errorData') }}
</p>
<div class="node-error-view__details-value">
<pre><code>{{ error.context.data }}</code></pre>
</div>
</div>
<div class="node-error-view__details-row" v-if="error.extra">
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.details.errorExtra') }}
</p>
<div class="node-error-view__details-value">
<pre><code>{{ error.extra }}</code></pre>
</div>
</div>
<div class="node-error-view__details-row" v-if="error.context && error.context.request">
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.details.request') }}
</p>
<div class="node-error-view__details-value">
<pre><code>{{ error.context.request }}</code></pre>
</div>
</div>
</div>
</details>
<details class="node-error-view__details">
<summary class="node-error-view__details-summary">
<font-awesome-icon class="node-error-view__details-icon" icon="angle-right" />
{{ $locale.baseText('nodeErrorView.details.info') }}
</summary>
<div class="node-error-view__details-content">
<div
class="node-error-view__details-row"
v-if="error.context && error.context.itemIndex !== undefined"
>
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.itemIndex') }}
</p>
<p class="node-error-view__details-value">
<code>{{ error.context.itemIndex }}</code>
</p>
</div>
<div
class="node-error-view__details-row"
v-if="error.context && error.context.runIndex !== undefined"
>
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.runIndex') }}
</p>
<p class="node-error-view__details-value">
<code>{{ error.context.runIndex }}</code>
</p>
</div>
<div
class="node-error-view__details-row"
v-if="error.context && error.context.parameter !== undefined"
>
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.inParameter') }}
</p>
<p class="node-error-view__details-value">
<code>{{ parameterDisplayName(error.context.parameter) }}</code>
</p>
</div>
<div class="node-error-view__details-row" v-if="error.node && error.node.type">
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.details.nodeType') }}
</p>
<p class="node-error-view__details-value">
<code>{{ error.node.type }}</code>
</p>
</div>
<div class="node-error-view__details-row" v-if="error.node && error.node.typeVersion">
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.details.nodeVersion') }}
</p>
<p class="node-error-view__details-value">
<code>
<span>{{ error.node.typeVersion + ' ' }}</span>
<span>({{ nodeVersionTag(error.node) }})</span>
</code>
</p>
</div>
<div class="node-error-view__details-row">
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.details.n8nVersion') }}
</p>
<p class="node-error-view__details-value">
<code>{{ n8nVersion }}</code>
</p>
</div>
<div class="node-error-view__details-row" v-if="error.timestamp">
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.time') }}
</p>
<p class="node-error-view__details-value">
<code>{{ new Date(error.timestamp).toLocaleString() }}</code>
</p>
</div>
<div class="node-error-view__details-row" v-if="error.cause && displayCause">
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.details.errorCause') }}
</p>
<pre class="node-error-view__details-value"><code>{{ error.cause }}</code></pre>
</div>
<div
class="node-error-view__details-row"
v-if="error.context && error.context.causeDetailed"
>
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.details.causeDetailed') }}
</p>
<pre
class="node-error-view__details-value"
><code>{{ error.context.causeDetailed }}</code></pre>
</div>
<div class="node-error-view__details-row" v-if="error.stack">
<p class="node-error-view__details-label">
{{ $locale.baseText('nodeErrorView.details.stackTrace') }}
</p>
<pre class="node-error-view__details-value"><code>{{ error.stack }}</code></pre>
</div>
</div>
</details>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import VueJsonPretty from 'vue-json-pretty';
import { useToast } from '@/composables/useToast';
import { MAX_DISPLAY_DATA_SIZE } from '@/constants';
@@ -133,13 +223,13 @@ import type {
import { sanitizeHtml } from '@/utils/htmlUtils';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useClipboard } from '@/composables/useClipboard';
import type { IDataObject } from 'n8n-workflow';
import type { INodeUi } from '@/Interface';
export default defineComponent({
name: 'NodeErrorView',
components: {
VueJsonPretty,
},
props: ['error'],
setup() {
const clipboard = useClipboard();
@@ -150,7 +240,7 @@ export default defineComponent({
};
},
computed: {
...mapStores(useNodeTypesStore, useNDVStore),
...mapStores(useNodeTypesStore, useNDVStore, useRootStore),
displayCause(): boolean {
return JSON.stringify(this.error.cause).length < MAX_DISPLAY_DATA_SIZE;
},
@@ -167,8 +257,67 @@ export default defineComponent({
return nodeType.properties;
},
n8nVersion() {
const baseUrl = this.rootStore.urlBaseEditor;
let instanceType = 'Self Hosted';
if (baseUrl.includes('n8n.cloud')) {
instanceType = 'Cloud';
}
return this.rootStore.versionCli + ` (${instanceType})`;
},
prepareRawMessages() {
const returnData: Array<string | IDataObject> = [];
if (!this.error.messages || !this.error.messages.length) {
return [];
}
const errorMessage = this.getErrorMessage();
(Array.from(new Set(this.error.messages)) as string[]).forEach((message) => {
const isParsable = /^\d{3} - \{/.test(message);
const parts = isParsable ? message.split(' - ').map((part) => part.trim()) : [];
//try to parse the message as JSON
for (const part of parts) {
try {
const parsed = JSON.parse(part);
if (typeof parsed === 'object') {
returnData.push(parsed);
return;
}
} catch (error) {}
}
//if message is the same as error message, do not include it
if (message === errorMessage) return;
returnData.push(message);
});
return returnData;
},
},
methods: {
getNodeDefaultName(node: INodeUi) {
if (!node) return 'Node';
const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
return nodeType?.defaults?.name || node.name;
},
nodeVersionTag(nodeType: IDataObject): string {
if (!nodeType || nodeType.hidden) {
return this.$locale.baseText('nodeSettings.deprecated');
}
const latestNodeVersion = Math.max(
...this.nodeTypesStore.getNodeVersions(nodeType.type as string),
);
if (latestNodeVersion === nodeType.typeVersion) {
return this.$locale.baseText('nodeSettings.latest');
}
return this.$locale.baseText('nodeSettings.latestVersion', {
interpolate: { version: latestNodeVersion.toString() },
});
},
replacePlaceholders(parameter: string, message: string): string {
const parameterName = this.parameterDisplayName(parameter, false);
const parameterFullName = this.parameterDisplayName(parameter, true);
@@ -199,7 +348,7 @@ export default defineComponent({
);
},
getErrorMessage(): string {
const baseErrorMessage = this.$locale.baseText('nodeErrorView.error') + ': ';
const baseErrorMessage = '';
const isSubNodeError =
this.error.name === 'NodeOperationError' &&
@@ -284,8 +433,89 @@ export default defineComponent({
// We can not resolve any deeper so lets stop here and at least return hopefully something useful
return [currentParameter];
},
copyCause() {
void this.clipboard.copy(JSON.stringify(this.error.cause));
copyErrorDetails() {
const error = this.error;
const errorInfo: IDataObject = {
errorMessage: this.getErrorMessage(),
};
if (error.description) {
errorInfo.errorDescription = error.description;
}
//add error details
const errorDetails: IDataObject = {};
if (error?.messages?.length) {
errorDetails.rawErrorMessage = error.messages;
}
if (error.httpCode) {
errorDetails.httpCode = error.httpCode;
}
if (error.context && error.context.data) {
errorDetails.errorData = error.context.data;
}
if (error.extra) {
errorDetails.errorExtra = error.extra;
}
errorInfo.errorDetails = errorDetails;
//add n8n details
const n8nDetails: IDataObject = {};
if (error.node) {
n8nDetails.nodeName = error.node.name;
n8nDetails.nodeType = error.node.type;
n8nDetails.nodeVersion = error.node.typeVersion;
if (error.node?.parameters?.resource) {
n8nDetails.resource = error.node.parameters.resource;
}
if (error?.node?.parameters?.operation) {
n8nDetails.operation = error.node.parameters.operation;
}
}
if (error.context) {
if (error.context.itemIndex !== undefined) {
n8nDetails.itemIndex = error.context.itemIndex;
}
if (error.context.runIndex !== undefined) {
n8nDetails.runIndex = error.context.runIndex;
}
if (error.context.parameter !== undefined) {
n8nDetails.parameter = error.context.parameter;
}
if (error.context.causeDetailed) {
n8nDetails.causeDetailed = error.context.causeDetailed;
}
}
if (error.timestamp) {
n8nDetails.time = new Date(error.timestamp).toLocaleString();
}
n8nDetails.n8nVersion = this.n8nVersion;
n8nDetails.binaryDataMode = this.rootStore.binaryDataMode;
if (error.cause) {
n8nDetails.cause = error.cause;
}
n8nDetails.stackTrace = error.stack && error.stack.split('\n');
errorInfo.n8nDetails = n8nDetails;
void this.clipboard.copy(JSON.stringify(errorInfo, null, 2));
this.copySuccess();
},
copySuccess() {
@@ -299,72 +529,124 @@ export default defineComponent({
</script>
<style lang="scss">
.error-header {
margin-bottom: 10px;
}
.node-error-view {
&__header {
max-width: 960px;
margin: 0 auto var(--spacing-s) auto;
padding-bottom: var(--spacing-3xs);
background-color: var(--color-background-xlight);
border: 1px solid var(--color-foreground-base);
border-radius: var(--border-radius-large);
}
.error-message {
color: var(--color-ndv-ouptut-error-font);
font-weight: bold;
font-size: 1.1rem;
}
&__header-title {
padding: var(--spacing-2xs) var(--spacing-s);
border-bottom: 1px solid var(--color-danger-tint-1);
font-size: var(--font-size-3xs);
font-weight: var(--font-weight-bold);
background-color: var(--color-danger-tint-2);
border-radius: var(--border-radius-large) var(--border-radius-large) 0 0;
color: var(--color-danger);
}
.error-description {
margin-top: 10px;
font-size: 1rem;
}
&__header-message {
padding: var(--spacing-xs) var(--spacing-s) var(--spacing-3xs) var(--spacing-s);
color: var(--color-danger);
color: var(--color-danger);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-s);
}
.error-details__summary {
font-weight: 600;
font-size: 16px;
cursor: pointer;
outline: none;
}
&__header-description {
padding: 0 var(--spacing-s) var(--spacing-3xs) var(--spacing-s);
font-size: var(--font-size-xs);
}
.error-details__icon {
margin-right: 4px;
}
&__info {
max-width: 960px;
margin: 0 auto;
border: 1px solid var(--color-foreground-base);
border-radius: var(--border-radius-large);
}
details > summary {
list-style-type: none;
}
&__info-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-3xs) var(--spacing-3xs) var(--spacing-3xs) var(--spacing-s);
border-bottom: 1px solid var(--color-foreground-base);
}
details > summary::-webkit-details-marker {
display: none;
}
&__info-title {
font-size: var(--font-size-2xs);
font-weight: var(--font-weight-bold);
color: var(--color-text-dark);
}
details[open] {
.error-details__icon {
transform: rotate(90deg);
&__info-content {
padding: var(--spacing-2xs) var(--spacing-s);
}
&__details:not(:last-child) {
margin-bottom: var(--spacing-2xs);
}
&__details[open] {
.node-error-view__details {
&-icon {
transform: rotate(90deg);
transition: transform 0.3s ease;
}
}
}
&__details-summary {
padding: var(--spacing-5xs) 0;
font-size: var(--font-size-2xs);
color: var(--color-text-dark);
cursor: pointer;
list-style-type: none;
outline: none;
}
&__details-content {
padding: var(--spacing-2xs) var(--spacing-s);
}
&__details-row {
display: flex;
padding: var(--spacing-4xs) 0;
}
&__details-row:not(:first-child) {
border-top: 1px solid var(--color-foreground-base);
}
&__details-icon {
margin-right: var(--spacing-xs);
}
&__details-label {
flex-grow: 0;
flex-shrink: 0;
width: 120px;
color: var(--color-text);
font-size: var(--font-size-2xs);
}
&__details-value {
flex: 1;
overflow: hidden;
margin-right: auto;
color: var(--color-text);
font-size: var(--font-size-2xs);
word-wrap: break-word;
code {
color: var(--color-json-string);
text-wrap: wrap;
word-wrap: break-word;
}
}
}
.error-details__content {
margin-top: 15px;
}
.el-divider__text {
background-color: var(--color-background-light);
}
.box-card {
margin-top: 1em;
overflow: auto;
}
.box-card__title {
font-weight: 400;
}
.box-card__subtitle {
font-weight: 200;
font-style: italic;
font-size: 0.7rem;
}
.copy-button {
position: absolute;
right: 50px;
z-index: 1000;
}
</style>

View File

@@ -178,7 +178,7 @@
((dataCount > 0 && maxRunIndex === 0) || search) &&
!isArtificialRecoveredEventItem
"
v-show="!editMode.enabled"
v-show="!editMode.enabled && !hasRunError"
:class="$style.itemsCount"
data-test-id="ndv-items-count"
>

View File

@@ -997,11 +997,27 @@
"nodeCredentials.updateCredential": "Update Credential",
"nodeErrorView.cause": "Cause",
"nodeErrorView.copyToClipboard": "Copy to Clipboard",
"nodeErrorView.copyToClipboard.tooltip": "Copy error details for debugging. Copied data may contain sensitive information. Proceed with caution when sharing.",
"nodeErrorView.dataBelowMayContain": "Data below may contain sensitive information. Proceed with caution when sharing.",
"nodeErrorView.details": "Details",
"nodeErrorView.details.from": "From {node}",
"nodeErrorView.details.rawMessages": "Full message",
"nodeErrorView.details.errorData": "Error data",
"nodeErrorView.details.errorExtra": "Error extra",
"nodeErrorView.details.request": "Request",
"nodeErrorView.details.title": "Error details",
"nodeErrorView.details.message": "Error message",
"nodeErrorView.details.info": "Other info",
"nodeErrorView.details.nodeVersion": "Node version",
"nodeErrorView.details.nodeType": "Node type",
"nodeErrorView.details.n8nVersion": "n8n version",
"nodeErrorView.details.errorCause": "Error cause",
"nodeErrorView.details.causeDetailed": "Cause detailed",
"nodeErrorView.details.stackTrace": "Stack trace",
"nodeErrorView.error": "ERROR",
"nodeErrorView.errorSubNode": "Error in sub-node {node}",
"nodeErrorView.httpCode": "HTTP Code",
"nodeErrorView.errorCode": "Error code",
"nodeErrorView.inParameter": "In or underneath Parameter",
"nodeErrorView.itemIndex": "Item Index",
"nodeErrorView.runIndex": "Run Index",

View File

@@ -30,6 +30,7 @@ export const useRootStore = defineStore(STORES.ROOT, {
urlBaseEditor: 'http://localhost:5678',
isNpmAvailable: false,
instanceId: '',
binaryDataMode: 'default',
}),
getters: {
getBaseUrl(): string {
@@ -128,5 +129,8 @@ export const useRootStore = defineStore(STORES.ROOT, {
setIsNpmAvailable(isNpmAvailable: boolean): void {
this.isNpmAvailable = isNpmAvailable;
},
setBinaryDataMode(binaryDataMode: string): void {
this.binaryDataMode = binaryDataMode;
},
},
});

View File

@@ -280,6 +280,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
rootStore.setN8nMetadata(settings.n8nMetadata || {});
rootStore.setDefaultLocale(settings.defaultLocale);
rootStore.setIsNpmAvailable(settings.isNpmAvailable);
rootStore.setBinaryDataMode(settings.binaryDataMode);
useVersionsStore().setVersionNotificationSettings(settings.versionNotifications);
},

View File

@@ -3,10 +3,12 @@ import type {
INodeExecutionData,
INodeProperties,
IExecuteFunctions,
NodeApiError,
} from 'n8n-workflow';
import { updateDisplayOptions, wrapData } from '../../../../../utils/utilities';
import { apiRequest } from '../../transport';
import { baseRLC } from '../common.descriptions';
import { processAirtableError } from '../../helpers/utils';
const properties: INodeProperties[] = [
{
@@ -45,6 +47,7 @@ export async function execute(
returnData.push(...executionData);
} catch (error) {
error = processAirtableError(error as NodeApiError, undefined, i);
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } });
continue;

View File

@@ -3,11 +3,12 @@ import type {
INodeExecutionData,
INodeProperties,
IExecuteFunctions,
NodeApiError,
} from 'n8n-workflow';
import { updateDisplayOptions, wrapData } from '../../../../../utils/utilities';
import { apiRequest } from '../../transport';
import { insertUpdateOptions } from '../common.descriptions';
import { removeIgnored } from '../../helpers/utils';
import { processAirtableError, removeIgnored } from '../../helpers/utils';
const properties: INodeProperties[] = [
{
@@ -85,6 +86,7 @@ export async function execute(
returnData.push(...executionData);
} catch (error) {
error = processAirtableError(error as NodeApiError, undefined, i);
if (this.continueOnFail()) {
returnData.push({ json: { message: error.message, error } });
continue;

View File

@@ -54,7 +54,7 @@ export async function execute(
returnData.push(...executionData);
} catch (error) {
error = processAirtableError(error as NodeApiError, id);
error = processAirtableError(error as NodeApiError, id, i);
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } });
continue;

View File

@@ -90,7 +90,7 @@ export async function execute(
returnData.push(...executionData);
} catch (error) {
error = processAirtableError(error as NodeApiError, id);
error = processAirtableError(error as NodeApiError, id, i);
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } });
continue;

View File

@@ -137,7 +137,7 @@ export async function execute(
returnData.push(...executionData);
} catch (error) {
error = processAirtableError(error as NodeApiError, recordId);
error = processAirtableError(error as NodeApiError, recordId, i);
if (this.continueOnFail()) {
returnData.push({ json: { message: error.message, error } });
continue;

View File

@@ -3,10 +3,11 @@ import type {
INodeExecutionData,
INodeProperties,
IExecuteFunctions,
NodeApiError,
} from 'n8n-workflow';
import { updateDisplayOptions, wrapData } from '../../../../../utils/utilities';
import { apiRequest, apiRequestAllItems, batchUpdate } from '../../transport';
import { removeIgnored } from '../../helpers/utils';
import { processAirtableError, removeIgnored } from '../../helpers/utils';
import type { UpdateRecord } from '../../helpers/interfaces';
import { insertUpdateOptions } from '../common.descriptions';
@@ -146,6 +147,7 @@ export async function execute(
returnData.push(...executionData);
} catch (error) {
error = processAirtableError(error as NodeApiError, undefined, i);
if (this.continueOnFail()) {
returnData.push({ json: { message: error.message, error } });
continue;

View File

@@ -1,5 +1,6 @@
import { ApplicationError, type IDataObject, type NodeApiError } from 'n8n-workflow';
import type { UpdateRecord } from './interfaces';
import set from 'lodash/set';
export function removeIgnored(data: IDataObject, ignore: string | string[]) {
if (ignore) {
@@ -66,13 +67,18 @@ export function findMatches(
}
}
export function processAirtableError(error: NodeApiError, id?: string) {
export function processAirtableError(error: NodeApiError, id?: string, itemIndex?: number) {
if (error.description === 'NOT_FOUND' && id) {
error.description = `${id} is not a valid Record ID`;
}
if (error.description?.includes('You must provide an array of up to 10 record objects') && id) {
error.description = `${id} is not a valid Record ID`;
}
if (itemIndex !== undefined) {
set(error, 'context.itemIndex', itemIndex);
}
return error;
}

View File

@@ -37,6 +37,7 @@ export async function calApiRequest(
try {
return await this.helpers.httpRequestWithAuthentication.call(this, 'calApi', options);
} catch (error) {
if (error instanceof NodeApiError) throw error;
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}

View File

@@ -13,6 +13,8 @@ import { PythonSandbox } from './PythonSandbox';
import { getSandboxContext } from './Sandbox';
import { standardizeOutput } from './utils';
import set from 'lodash/set';
const { CODE_ENABLE_STDOUT } = process.env;
export class Code implements INodeType {
@@ -133,7 +135,10 @@ export class Code implements INodeType {
try {
items = (await sandbox.runCodeAllItems()) as INodeExecutionData[];
} catch (error) {
if (!this.continueOnFail()) throw error;
if (!this.continueOnFail()) {
set(error, 'node', node);
throw error;
}
items = [{ json: { error: error.message } }];
}
@@ -158,7 +163,10 @@ export class Code implements INodeType {
try {
result = await sandbox.runCodeEachItem();
} catch (error) {
if (!this.continueOnFail()) throw error;
if (!this.continueOnFail()) {
set(error, 'node', node);
throw error;
}
returnData.push({
json: { error: error.message },
pairedItem: {

View File

@@ -86,6 +86,8 @@ export class FilterV2 implements INodeType {
"Try to change the operator, switch ON the option 'Less Strict Type Validation', or change the type with an expression",
);
}
set(error, 'context.itemIndex', itemIndex);
set(error, 'node', this.getNode());
throw error;
}

View File

@@ -57,31 +57,13 @@ export async function googleApiRequest(
);
}
} catch (error) {
if (error instanceof NodeApiError) throw error;
if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') {
error.statusCode = '401';
}
const apiError = new NodeApiError(
this.getNode(),
{
reason: error.error,
} as JsonObject,
{ httpCode: String(error.statusCode) },
);
if (
apiError.message &&
apiError.description &&
(apiError.message.toLowerCase().includes('bad request') ||
apiError.message.toLowerCase().includes('forbidden') ||
apiError.message.toUpperCase().includes('UNKNOWN ERROR'))
) {
const message = apiError.message;
apiError.message = apiError.description;
apiError.description = message;
}
throw apiError;
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}

View File

@@ -1,4 +1,4 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { type IExecuteFunctions, type IDataObject, type INodeExecutionData } from 'n8n-workflow';
import { GoogleSheet } from '../helpers/GoogleSheet';
import { getSpreadsheetId } from '../helpers/GoogleSheets.utils';
import type { GoogleSheets, ResourceLocator } from '../helpers/GoogleSheets.types';
@@ -72,20 +72,11 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
if (results?.length) {
operationResult = operationResult.concat(results);
}
} catch (err) {
} catch (error) {
if (this.continueOnFail()) {
operationResult.push({ json: this.getInputData(0)[0].json, error: err });
operationResult.push({ json: this.getInputData(0)[0].json, error });
} else {
if (
err.message &&
(err.message.toLowerCase().includes('bad request') ||
err.message.toLowerCase().includes('uknown error')) &&
err.description
) {
err.message = err.description;
err.description = undefined;
}
throw err;
throw error;
}
}

View File

@@ -5,7 +5,7 @@ import { wrapData } from '../../../../../../utils/utilities';
export async function execute(
this: IExecuteFunctions,
sheet: GoogleSheet,
_sheet: GoogleSheet,
sheetName: string,
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];

View File

@@ -139,11 +139,8 @@ export class GoogleSheet {
});
if (!foundItem?.properties?.title) {
throw new NodeOperationError(
node,
`Sheet with ${mode === 'name' ? 'name' : 'ID'} ${value} not found`,
{ level: 'warning' },
);
const error = new Error(`Sheet with ${mode === 'name' ? 'name' : 'ID'} ${value} not found`);
throw new NodeOperationError(node, error, { level: 'warning' });
}
return foundItem.properties;

View File

@@ -9,6 +9,7 @@ import type {
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { getGoogleAccessToken } from '../../../GenericFunctions';
import set from 'lodash/set';
export async function apiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
@@ -62,11 +63,15 @@ export async function apiRequest(
error.statusCode = '401';
}
if (error.message.includes('PERMISSION_DENIED')) {
const message = `Missing permissions for Google Sheet, ${error.message}}`;
const details = error.description ? ` Details of the error: ${error.description}.` : '';
const description = `Please check that the account you're using has the right permissions. (If you're trying to modify the sheet, you'll need edit access.)${details}`;
throw new NodeApiError(this.getNode(), error as JsonObject, { message, description });
if (error instanceof NodeApiError) {
if (error.message.includes('PERMISSION_DENIED')) {
const details = error.description ? ` Details of the error: ${error.description}.` : '';
const description = `Please check that the account you're using has the right permissions. (If you're trying to modify the sheet, you'll need edit access.)${details}`;
set(error, 'description', description);
}
throw error;
}
throw new NodeApiError(this.getNode(), error as JsonObject);

View File

@@ -34,6 +34,7 @@ import {
sanitizeUiMessage,
} from '../GenericFunctions';
import { keysToLowercase } from '@utils/utilities';
import set from 'lodash/set';
function toText<T>(data: T) {
if (typeof data === 'object' && data !== null) {
@@ -1255,6 +1256,7 @@ export class HttpRequestV3 implements INodeType {
requestInterval: number;
};
const sanitazedRequests: IDataObject[] = [];
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (authentication === 'genericCredentialType') {
genericCredentialType = this.getNodeParameter('genericAuthType', 0) as string;
@@ -1627,8 +1629,11 @@ export class HttpRequestV3 implements INodeType {
'application/json,text/html,application/xhtml+xml,application/xml,text/*;q=0.9, image/*;q=0.8, */*;q=0.7';
}
}
try {
this.sendMessageToUI(sanitizeUiMessage(requestOptions, authDataKeys));
const sanitazedRequestOptions = sanitizeUiMessage(requestOptions, authDataKeys);
this.sendMessageToUI(sanitazedRequestOptions);
sanitazedRequests.push(sanitazedRequestOptions);
} catch (e) {}
if (pagination && pagination.paginationMode !== 'off') {
@@ -1770,7 +1775,9 @@ export class HttpRequestV3 implements INodeType {
if (autoDetectResponseFormat && responseData.reason.error instanceof Buffer) {
responseData.reason.error = Buffer.from(responseData.reason.error as Buffer).toString();
}
throw new NodeApiError(this.getNode(), responseData as JsonObject, { itemIndex });
const error = new NodeApiError(this.getNode(), responseData as JsonObject, { itemIndex });
set(error, 'context.request', sanitazedRequests[itemIndex]);
throw error;
} else {
removeCircularRefs(responseData.reason as JsonObject);
// Return the actual reason as error

View File

@@ -15,7 +15,7 @@ import type {
INodeTypeDescription,
JsonObject,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import { snakeCase } from 'change-case';
import { generatePairedItemData } from '../../../utils/utilities';
@@ -47,6 +47,8 @@ import type { IForm } from './FormInterface';
import type { IAssociation, IDeal } from './DealInterface';
import set from 'lodash/set';
export class HubspotV2 implements INodeType {
description: INodeTypeDescription;
@@ -3058,21 +3060,17 @@ export class HubspotV2 implements INodeType {
error.cause.error?.validationResults &&
error.cause.error.validationResults[0].error === 'INVALID_EMAIL'
) {
throw new NodeOperationError(
this.getNode(),
error.cause.error.validationResults[0].message as string,
);
const message = error.cause.error.validationResults[0].message as string;
set(error, 'message', message);
}
if (error.cause.error?.message !== 'The resource you are requesting could not be found') {
if (error.httpCode === '404' && error.description === 'resource not found') {
throw new NodeOperationError(
this.getNode(),
`${error.node.parameters.resource} #${
error.node.parameters[`${error.node.parameters.resource}Id`].value
} could not be found. Check your ${error.node.parameters.resource} ID is correct`,
);
const message = `${error.node.parameters.resource} #${
error.node.parameters[`${error.node.parameters.resource}Id`].value
} could not be found. Check your ${error.node.parameters.resource} ID is correct`;
set(error, 'message', message);
}
throw new NodeOperationError(this.getNode(), error as string);
}
if (this.continueOnFail()) {
returnData.push({
@@ -3081,6 +3079,9 @@ export class HubspotV2 implements INodeType {
});
continue;
}
if (error instanceof NodeApiError) {
set(error, 'context.itemIndex', i);
}
throw error;
}
}

View File

@@ -86,6 +86,8 @@ export class IfV2 implements INodeType {
"Try to change the operator, switch ON the option 'Less Strict Type Validation', or change the type with an expression",
);
}
set(error, 'context.itemIndex', itemIndex);
set(error, 'node', this.getNode());
throw error;
}

View File

@@ -11,6 +11,7 @@ import type {
} from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import { capitalize } from '@utils/utilities';
import set from 'lodash/set';
const configuredOutputs = (parameters: INodeParameters) => {
const mode = parameters.mode as string;
@@ -351,6 +352,8 @@ export class SwitchV3 implements INodeType {
error.description =
"Try to change the operator, switch ON the option 'Less Strict Type Validation', or change the type with an expression";
}
set(error, 'context.itemIndex', itemIndex);
set(error, 'node', this.getNode());
throw error;
}

View File

@@ -28,6 +28,7 @@ export async function uprocApiRequest(
try {
return await this.helpers.httpRequestWithAuthentication.call(this, 'uprocApi', options);
} catch (error) {
if (error instanceof NodeApiError) throw error;
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}

View File

@@ -12,6 +12,11 @@ export const CREDENTIAL_EMPTY_VALUE =
export const FORM_TRIGGER_PATH_IDENTIFIER = 'n8n-form';
export const UNKNOWN_ERROR_MESSAGE = 'There was an unknown issue while executing the node';
export const UNKNOWN_ERROR_DESCRIPTION =
'Double-check the node configuration and the service it connects to. Check the error details below and refer to the <a href="https://docs.n8n.io" target="_blank">n8n documentation</a> to troubleshoot the issue.';
export const UNKNOWN_ERROR_MESSAGE_CRED = 'UNKNOWN ERROR';
//n8n-nodes-base
export const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote';
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';

View File

@@ -2474,6 +2474,7 @@ export interface IN8nUISettings {
urlBaseWebhook: string;
urlBaseEditor: string;
versionCli: string;
binaryDataMode: string;
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev';
n8nMetadata?: {
userId?: string;

View File

@@ -227,15 +227,19 @@ export class RoutingNode {
continue;
}
if (error instanceof NodeApiError) {
set(error, 'context.itemIndex', i);
set(error, 'context.runIndex', runIndex);
throw error;
}
interface AxiosError extends NodeError {
isAxiosError: boolean;
description: string | undefined;
response?: { status: number };
}
let routingError = error as AxiosError;
if (error instanceof NodeApiError && error.cause) routingError = error.cause as AxiosError;
const routingError = error as AxiosError;
throw new NodeApiError(this.node, error as JsonObject, {
runIndex,

View File

@@ -35,6 +35,8 @@ const COMMON_ERRORS: IDataObject = {
* a value recursively inside an error object.
*/
export abstract class NodeError extends ExecutionBaseError {
messages: string[] = [];
constructor(
readonly node: INode,
error: Error | JsonObject,
@@ -123,52 +125,56 @@ export abstract class NodeError extends ExecutionBaseError {
return null;
}
/**
* Preserve the original error message before setting the new one
*/
protected addToMessages(message: string): void {
if (message && !this.messages.includes(message)) {
this.messages.push(message);
}
}
/**
* Set descriptive error message if code is provided or if message contains any of the common errors,
* update description to include original message plus the description
*/
protected setDescriptiveErrorMessage(
message: string,
description: string | undefined | null,
messages: string[],
code?: string | null,
messageMapping?: { [key: string]: string },
) {
): [string, string[]] {
let newMessage = message;
let newDescription = description as string;
if (messageMapping) {
for (const [mapKey, mapMessage] of Object.entries(messageMapping)) {
if ((message || '').toUpperCase().includes(mapKey.toUpperCase())) {
newMessage = mapMessage;
newDescription = this.updateDescription(message, description);
messages.push(message);
break;
}
}
if (newMessage !== message) {
return [newMessage, newDescription];
return [newMessage, messages];
}
}
// if code is provided and it is in the list of common errors set the message and return early
if (code && COMMON_ERRORS[code.toUpperCase()]) {
newMessage = COMMON_ERRORS[code] as string;
newDescription = this.updateDescription(message, description);
return [newMessage, newDescription];
messages.push(message);
return [newMessage, messages];
}
// check if message contains any of the common errors and set the message and description
for (const [errorCode, errorDescriptiveMessage] of Object.entries(COMMON_ERRORS)) {
if ((message || '').toUpperCase().includes(errorCode.toUpperCase())) {
newMessage = errorDescriptiveMessage as string;
newDescription = this.updateDescription(message, description);
messages.push(message);
break;
}
}
return [newMessage, newDescription];
}
protected updateDescription(message: string, description: string | undefined | null) {
return `${message}${description ? ` - ${description}` : ''}`;
return [newMessage, messages];
}
}

View File

@@ -15,7 +15,12 @@ import { NodeError } from './abstract/node.error';
import { removeCircularRefs } from '../utils';
import type { ReportingOptions } from './application.error';
import { AxiosError } from 'axios';
import { NO_OP_NODE_TYPE } from '../Constants';
import {
NO_OP_NODE_TYPE,
UNKNOWN_ERROR_DESCRIPTION,
UNKNOWN_ERROR_MESSAGE,
UNKNOWN_ERROR_MESSAGE_CRED,
} from '../Constants';
export interface NodeOperationErrorOptions {
message?: string;
@@ -103,9 +108,6 @@ const STATUS_CODE_MESSAGES: IStatusCodeMessages = {
'504': 'Gateway timed out - perhaps try again later?',
};
const UNKNOWN_ERROR_MESSAGE = 'UNKNOWN ERROR - check the detailed error for more information';
const UNKNOWN_ERROR_MESSAGE_CRED = 'UNKNOWN ERROR';
/**
* Class for instantiating an error in an API response, e.g. a 404 Not Found response,
* with an HTTP error code, an error message and a description.
@@ -130,6 +132,8 @@ export class NodeApiError extends NodeError {
) {
super(node, errorResponse);
this.addToMessages(errorResponse.message as string);
if (!httpCode && errorResponse instanceof AxiosError) {
httpCode = errorResponse.response?.status?.toString();
}
@@ -176,6 +180,8 @@ export class NodeApiError extends NodeError {
// set http code of this error
if (httpCode) {
this.httpCode = httpCode;
} else if (errorResponse.httpCode) {
this.httpCode = errorResponse.httpCode as string;
} else {
this.httpCode =
this.findProperty(errorResponse, ERROR_STATUS_PROPERTIES, ERROR_NESTING_PROPERTIES) ?? null;
@@ -187,6 +193,25 @@ export class NodeApiError extends NodeError {
this.level = 'warning';
}
if (
errorResponse?.response &&
typeof errorResponse?.response === 'object' &&
!Array.isArray(errorResponse.response) &&
errorResponse.response.data &&
typeof errorResponse.response.data === 'object' &&
!Array.isArray(errorResponse.response.data)
) {
const data = errorResponse.response.data;
if (data.message) {
description = data.message as string;
} else if (data.error && ((data.error as IDataObject) || {}).message) {
description = (data.error as IDataObject).message as string;
}
this.context.data = data;
}
// set description of this error
if (description) {
this.description = description;
@@ -204,7 +229,9 @@ export class NodeApiError extends NodeError {
}
}
// set message if provided or set default message based on http code
// set message if provided
// set default message based on http code
// or use raw error message
if (message) {
this.message = message;
} else {
@@ -217,9 +244,9 @@ export class NodeApiError extends NodeError {
}
// if message contain common error code set descriptive message and update description
[this.message, this.description] = this.setDescriptiveErrorMessage(
[this.message, this.messages] = this.setDescriptiveErrorMessage(
this.message,
this.description,
this.messages,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
this.httpCode ||
(errorResponse?.code as string) ||
@@ -259,29 +286,44 @@ export class NodeApiError extends NodeError {
if (!this.httpCode) {
this.httpCode = null;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
this.message = this.message || this.description || UNKNOWN_ERROR_MESSAGE;
if (!this.message) {
if (this.description) {
this.message = this.description;
this.description = undefined;
} else {
this.message = UNKNOWN_ERROR_MESSAGE;
this.description = UNKNOWN_ERROR_DESCRIPTION;
}
}
return;
}
if (STATUS_CODE_MESSAGES[this.httpCode]) {
this.description = this.updateDescription(this.message, this.description);
this.addToMessages(this.message);
this.message = STATUS_CODE_MESSAGES[this.httpCode];
return;
}
switch (this.httpCode.charAt(0)) {
case '4':
this.description = this.updateDescription(this.message, this.description);
this.addToMessages(this.message);
this.message = STATUS_CODE_MESSAGES['4XX'];
break;
case '5':
this.description = this.updateDescription(this.message, this.description);
this.addToMessages(this.message);
this.message = STATUS_CODE_MESSAGES['5XX'];
break;
default:
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
this.message = this.message || this.description || UNKNOWN_ERROR_MESSAGE;
if (!this.message) {
if (this.description) {
this.message = this.description;
this.description = undefined;
} else {
this.message = UNKNOWN_ERROR_MESSAGE;
this.description = UNKNOWN_ERROR_DESCRIPTION;
}
}
}
if (this.node.type === NO_OP_NODE_TYPE && this.message === UNKNOWN_ERROR_MESSAGE) {
this.message = `${UNKNOWN_ERROR_MESSAGE_CRED} - ${this.httpCode}`;

View File

@@ -20,6 +20,10 @@ export class NodeOperationError extends NodeError {
}
super(node, error);
if (error instanceof NodeError && error?.messages?.length) {
error.messages.forEach((message) => this.addToMessages(message));
}
if (options.message) this.message = options.message;
if (options.level) this.level = options.level;
if (options.functionality) this.functionality = options.functionality;
@@ -32,9 +36,9 @@ export class NodeOperationError extends NodeError {
this.description = undefined;
}
[this.message, this.description] = this.setDescriptiveErrorMessage(
[this.message, this.messages] = this.setDescriptiveErrorMessage(
this.message,
this.description,
this.messages,
undefined,
options.messageMapping,
);

View File

@@ -1,6 +1,7 @@
import type { INode } from '@/Interfaces';
import { NodeOperationError } from '@/errors';
import { NodeApiError } from '@/errors/node-api.error';
import { UNKNOWN_ERROR_DESCRIPTION, UNKNOWN_ERROR_MESSAGE } from '../src/Constants';
const node: INode = {
id: '1',
@@ -17,9 +18,7 @@ describe('NodeErrors tests', () => {
it('should return unknown error message', () => {
const nodeApiError = new NodeApiError(node, {});
expect(nodeApiError.message).toEqual(
'UNKNOWN ERROR - check the detailed error for more information',
);
expect(nodeApiError.message).toEqual(UNKNOWN_ERROR_MESSAGE);
});
it('should return the error message', () => {
@@ -110,9 +109,8 @@ describe('NodeErrors tests', () => {
expect(nodeOperationError.message).toEqual('The server closed the connection unexpectedly');
expect(nodeOperationError.description).toEqual(
'GETADDRINFO test error message - test error description',
);
//description should not include error message
expect(nodeOperationError.description).toEqual('test error description');
});
it('should remove description if it is equal to message, NodeOperationError', () => {
@@ -175,3 +173,91 @@ describe('NodeErrors tests', () => {
);
});
});
describe('NodeApiError message and description logic', () => {
it('case: customMessage && customDescription, result: message === customMessage; description === customDescription', () => {
const apiError = { message: 'Original message', code: 404 };
const nodeApiError = new NodeApiError(node, apiError, {
message: 'Custom message',
description: 'Custom description',
});
expect(nodeApiError.message).toEqual('Custom message');
expect(nodeApiError.description).toEqual('Custom description');
expect(nodeApiError.messages).toContain('Original message');
});
it('case: customMessage && !customDescription && extractedMessage, result: message === customMessage; description === extractedMessage', () => {
const apiError = {
message: 'Original message',
code: 404,
response: { data: { error: { message: 'Extracted message' } } },
};
const nodeApiError = new NodeApiError(node, apiError, {
message: 'Custom message',
});
expect(nodeApiError.message).toEqual('Custom message');
expect(nodeApiError.description).toEqual('Extracted message');
expect(nodeApiError.messages).toContain('Original message');
});
it('case: customMessage && !customDescription && !extractedMessage, result: message === customMessage; !description', () => {
const apiError = {
message: '',
code: 404,
response: { data: { error: { foo: 'Extracted message' } } },
};
const nodeApiError = new NodeApiError(node, apiError, {
message: 'Custom message',
});
expect(nodeApiError.message).toEqual('Custom message');
expect(nodeApiError.description).toBeFalsy();
expect(nodeApiError.messages.length).toBe(0);
});
it('case: !customMessage && httpCodeMapping && extractedMessage, result: message === httpCodeMapping; description === extractedMessage', () => {
const apiError = {
message: 'Original message',
code: 404,
response: { data: { error: { message: 'Extracted message' } } },
};
const nodeApiError = new NodeApiError(node, apiError);
expect(nodeApiError.message).toEqual('The resource you are requesting could not be found');
expect(nodeApiError.description).toEqual('Extracted message');
expect(nodeApiError.messages).toContain('Original message');
});
it('case: !customMessage && httpCodeMapping && !extractedMessage, result: message === httpCodeMapping; !description', () => {
const apiError = {
message: '',
code: 500,
};
const nodeApiError = new NodeApiError(node, apiError);
expect(nodeApiError.message).toEqual('The service was not able to process your request');
expect(nodeApiError.description).toBeFalsy();
});
it('case: !customMessage && !httpCodeMapping && extractedMessage, result: message === extractedMessage; !description', () => {
const apiError = {
message: '',
code: 300,
response: { data: { error: { message: 'Extracted message' } } },
};
const nodeApiError = new NodeApiError(node, apiError);
expect(nodeApiError.message).toEqual('Extracted message');
expect(nodeApiError.description).toBeFalsy();
});
it('case: !customMessage && !httpCodeMapping && !extractedMessage, result: message === UNKNOWN_ERROR_MESSAGE; description === UNKNOWN_ERROR_DESCRIPTION', () => {
const apiError = {};
const nodeApiError = new NodeApiError(node, apiError);
expect(nodeApiError.message).toEqual(UNKNOWN_ERROR_MESSAGE);
expect(nodeApiError.description).toEqual(UNKNOWN_ERROR_DESCRIPTION);
});
});

View File

@@ -7,7 +7,7 @@ describe('NodeError', () => {
const node = mock<INode>();
it('should update re-wrapped error level and message', () => {
const apiError = new NodeApiError(node, mock({ message: 'Some error happened', code: 500 }));
const apiError = new NodeApiError(node, { message: 'Some error happened', code: 500 });
const opsError = new NodeOperationError(node, mock(), { message: 'Some operation failed' });
const wrapped1 = new NodeOperationError(node, apiError);
const wrapped2 = new NodeOperationError(node, opsError);