mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(editor): Improve errors in output panel (#8644)
Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -92,6 +92,7 @@ const defaultSettings: IN8nUISettings = {
|
||||
banners: {
|
||||
dismissed: [],
|
||||
},
|
||||
binaryDataMode: 'default',
|
||||
};
|
||||
|
||||
export function routesForSettings(server: Server) {
|
||||
|
||||
@@ -144,4 +144,5 @@ export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
|
||||
saveDataErrorExecution: 'all',
|
||||
saveDataSuccessExecution: 'all',
|
||||
saveManualExecutions: false,
|
||||
binaryDataMode: 'default',
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -2474,6 +2474,7 @@ export interface IN8nUISettings {
|
||||
urlBaseWebhook: string;
|
||||
urlBaseEditor: string;
|
||||
versionCli: string;
|
||||
binaryDataMode: string;
|
||||
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev';
|
||||
n8nMetadata?: {
|
||||
userId?: string;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user