From 817bf7d211e314d32f7363385817ea0b5ad5e13c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Oct 2025 10:48:11 +0000 Subject: [PATCH] fix: Add defensive response validation for n8n API list operations (issue #349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses "Cannot read properties of undefined (reading 'map')" error by adding validation and fallback handling for n8n API responses. Changes: - Add response structure validation in listWorkflows, listExecutions, listCredentials, and listTags methods - Handle edge case where API returns array directly instead of {data: [], nextCursor} wrapper object - Provide clear error messages when response format is unexpected - Add logging when using fallback format handling This fix ensures compatibility with different n8n API versions and prevents runtime errors when the response structure varies from expected. Fixes #349 Conceived by Romuald Członkowski - www.aiadvisors.pl/en --- src/services/n8n-api-client.ts | 92 ++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/src/services/n8n-api-client.ts b/src/services/n8n-api-client.ts index fe31d3f..9ee41cc 100644 --- a/src/services/n8n-api-client.ts +++ b/src/services/n8n-api-client.ts @@ -173,7 +173,28 @@ export class N8nApiClient { async listWorkflows(params: WorkflowListParams = {}): Promise { try { const response = await this.client.get('/workflows', { params }); - return response.data; + const responseData = response.data; + + // Validate response structure + if (!responseData || typeof responseData !== 'object') { + throw new Error('Invalid response from n8n API: response is not an object'); + } + + // Handle case where response.data is an array (older n8n versions or different API format) + if (Array.isArray(responseData)) { + logger.warn('n8n API returned array directly instead of {data, nextCursor} object. Wrapping in expected format.'); + return { + data: responseData, + nextCursor: null + }; + } + + // Validate expected format {data: [], nextCursor?: string} + if (!Array.isArray(responseData.data)) { + throw new Error(`Invalid response from n8n API: expected {data: [], nextCursor?: string}, got: ${JSON.stringify(Object.keys(responseData))}`); + } + + return responseData; } catch (error) { throw handleN8nApiError(error); } @@ -194,7 +215,28 @@ export class N8nApiClient { async listExecutions(params: ExecutionListParams = {}): Promise { try { const response = await this.client.get('/executions', { params }); - return response.data; + const responseData = response.data; + + // Validate response structure + if (!responseData || typeof responseData !== 'object') { + throw new Error('Invalid response from n8n API: response is not an object'); + } + + // Handle case where response.data is an array (older n8n versions or different API format) + if (Array.isArray(responseData)) { + logger.warn('n8n API returned array directly instead of {data, nextCursor} object for executions. Wrapping in expected format.'); + return { + data: responseData, + nextCursor: null + }; + } + + // Validate expected format {data: [], nextCursor?: string} + if (!Array.isArray(responseData.data)) { + throw new Error(`Invalid response from n8n API for executions: expected {data: [], nextCursor?: string}, got: ${JSON.stringify(Object.keys(responseData))}`); + } + + return responseData; } catch (error) { throw handleN8nApiError(error); } @@ -264,7 +306,28 @@ export class N8nApiClient { async listCredentials(params: CredentialListParams = {}): Promise { try { const response = await this.client.get('/credentials', { params }); - return response.data; + const responseData = response.data; + + // Validate response structure + if (!responseData || typeof responseData !== 'object') { + throw new Error('Invalid response from n8n API: response is not an object'); + } + + // Handle case where response.data is an array (older n8n versions or different API format) + if (Array.isArray(responseData)) { + logger.warn('n8n API returned array directly instead of {data, nextCursor} object for credentials. Wrapping in expected format.'); + return { + data: responseData, + nextCursor: null + }; + } + + // Validate expected format {data: [], nextCursor?: string} + if (!Array.isArray(responseData.data)) { + throw new Error(`Invalid response from n8n API for credentials: expected {data: [], nextCursor?: string}, got: ${JSON.stringify(Object.keys(responseData))}`); + } + + return responseData; } catch (error) { throw handleN8nApiError(error); } @@ -309,7 +372,28 @@ export class N8nApiClient { async listTags(params: TagListParams = {}): Promise { try { const response = await this.client.get('/tags', { params }); - return response.data; + const responseData = response.data; + + // Validate response structure + if (!responseData || typeof responseData !== 'object') { + throw new Error('Invalid response from n8n API: response is not an object'); + } + + // Handle case where response.data is an array (older n8n versions or different API format) + if (Array.isArray(responseData)) { + logger.warn('n8n API returned array directly instead of {data, nextCursor} object for tags. Wrapping in expected format.'); + return { + data: responseData, + nextCursor: null + }; + } + + // Validate expected format {data: [], nextCursor?: string} + if (!Array.isArray(responseData.data)) { + throw new Error(`Invalid response from n8n API for tags: expected {data: [], nextCursor?: string}, got: ${JSON.stringify(Object.keys(responseData))}`); + } + + return responseData; } catch (error) { throw handleN8nApiError(error); }