diff --git a/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts index 53682b1a9cab5..1298efa6ba724 100644 --- a/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts +++ b/packages/nodes-base/nodes/GraphQL/GraphQL.node.ts @@ -514,6 +514,46 @@ export class GraphQL implements INodeType { } else { response = await this.helpers.request(requestOptions); } + + // Parse string responses + if (typeof response === 'string' && responseFormat !== 'string') { + try { + response = JSON.parse(response); + } catch (error) { + throw new NodeOperationError( + this.getNode(), + 'Response body is not valid JSON. Change "Response Format" to "String"', + { itemIndex }, + ); + } + } + + // Check for GraphQL errors BEFORE adding to returnItems + let parsedForErrorCheck = response; + if (typeof response === 'string') { + try { + parsedForErrorCheck = JSON.parse(response); + } catch { + // Not JSON, no errors to check + parsedForErrorCheck = response; + } + } + + if ( + typeof parsedForErrorCheck === 'object' && + parsedForErrorCheck !== null && + 'errors' in parsedForErrorCheck + ) { + const errors = parsedForErrorCheck.errors; + if (Array.isArray(errors) && errors.length > 0) { + const message = + errors.map((error: IDataObject) => error.message).join(', ') || 'Unexpected error'; + const errorPayload: JsonObject = { errors }; + throw new NodeApiError(this.getNode(), errorPayload, { message }); + } + } + + // Only add to returnItems if there are no GraphQL errors if (responseFormat === 'string') { const dataPropertyName = this.getNodeParameter('dataPropertyName', 0); returnItems.push({ @@ -522,41 +562,12 @@ export class GraphQL implements INodeType { }, }); } else { - if (typeof response === 'string') { - try { - response = JSON.parse(response); - } catch (error) { - throw new NodeOperationError( - this.getNode(), - 'Response body is not valid JSON. Change "Response Format" to "String"', - { itemIndex }, - ); - } - } - const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray(response as IDataObject), { itemData: { item: itemIndex } }, ); returnItems.push(...executionData); } - - // parse error string messages - if (typeof response === 'string' && response.startsWith('{"errors":')) { - try { - const errorResponse = JSON.parse(response) as IDataObject; - if (Array.isArray(errorResponse.errors)) { - response = errorResponse; - } - } catch (e) {} - } - // throw from response object.errors[] - if (typeof response === 'object' && response.errors) { - const message = - response.errors?.map((error: IDataObject) => error.message).join(', ') || - 'Unexpected error'; - throw new NodeApiError(this.getNode(), response.errors as JsonObject, { message }); - } } catch (error) { if (!this.continueOnFail()) { throw error; @@ -565,10 +576,10 @@ export class GraphQL implements INodeType { const errorData = this.helpers.returnJsonArray({ error: error.message, }); - const exectionErrorWithMetaData = this.helpers.constructExecutionMetaData(errorData, { + const executionErrorWithMetaData = this.helpers.constructExecutionMetaData(errorData, { itemData: { item: itemIndex }, }); - returnItems.push(...exectionErrorWithMetaData); + returnItems.push(...executionErrorWithMetaData); } } return [returnItems]; diff --git a/packages/nodes-base/nodes/GraphQL/test/GraphQL.node.test.ts b/packages/nodes-base/nodes/GraphQL/test/GraphQL.node.test.ts index 97785f909a966..be5ec8a8fbc0a 100644 --- a/packages/nodes-base/nodes/GraphQL/test/GraphQL.node.test.ts +++ b/packages/nodes-base/nodes/GraphQL/test/GraphQL.node.test.ts @@ -110,4 +110,49 @@ describe('GraphQL Node', () => { credentials, }); }); + + describe('error output routing with continueOnFail', () => { + const baseUrl = 'http://test.example.com'; + + // Mock the first request that returns a GraphQL error + nock(baseUrl) + .post('/graphql', '{"query":"INVALID_QUERY","variables":{},"operationName":null}') + .reply(200, { + errors: [ + { + message: 'Syntax Error: Invalid query syntax', + extensions: { + code: 'GRAPHQL_PARSE_FAILED', + }, + }, + ], + }); + + // Mock the second request that returns successful data + nock(baseUrl) + .post( + '/graphql', + '{"query":"query { users { id name email } }","variables":{},"operationName":null}', + ) + .reply(200, { + data: { + users: [ + { + id: '1', + name: 'John Doe', + email: 'john@example.com', + }, + { + id: '2', + name: 'Jane Smith', + email: 'jane@example.com', + }, + ], + }, + }); + + new NodeTestHarness().setupTests({ + workflowFiles: ['workflow.error_output_routing.json'], + }); + }); }); diff --git a/packages/nodes-base/nodes/GraphQL/test/workflow.error_output_routing.json b/packages/nodes-base/nodes/GraphQL/test/workflow.error_output_routing.json new file mode 100644 index 0000000000000..8408c107320d2 --- /dev/null +++ b/packages/nodes-base/nodes/GraphQL/test/workflow.error_output_routing.json @@ -0,0 +1,119 @@ +{ + "name": "Test GraphQL Error Output Routing", + "nodes": [ + { + "parameters": {}, + "id": "manual-trigger", + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [0, 0] + }, + { + "parameters": { + "jsCode": "return [\n // This query will fail with a GraphQL error\n {\n query: 'INVALID_QUERY'\n },\n // This query will succeed\n {\n query: 'query { users { id name email } }'\n }\n]" + }, + "id": "code-node", + "name": "Generate Queries", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [200, 0] + }, + { + "parameters": { + "endpoint": "http://test.example.com/graphql", + "query": "={{ $json.query }}" + }, + "id": "graphql-node", + "name": "GraphQL", + "type": "n8n-nodes-base.graphql", + "typeVersion": 1.1, + "position": [400, 0], + "onError": "continueErrorOutput" + }, + { + "parameters": {}, + "id": "success-output", + "name": "Success Output", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [600, -100] + }, + { + "parameters": {}, + "id": "error-output", + "name": "Error Output", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [600, 100] + } + ], + "connections": { + "Manual Trigger": { + "main": [ + [ + { + "node": "Generate Queries", + "type": "main", + "index": 0 + } + ] + ] + }, + "Generate Queries": { + "main": [ + [ + { + "node": "GraphQL", + "type": "main", + "index": 0 + } + ] + ] + }, + "GraphQL": { + "main": [ + [ + { + "node": "Success Output", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Error Output", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "Success Output": [ + { + "data": { + "users": [ + { + "id": "1", + "name": "John Doe", + "email": "john@example.com" + }, + { + "id": "2", + "name": "Jane Smith", + "email": "jane@example.com" + } + ] + } + } + ], + "Error Output": [ + { + "query": "INVALID_QUERY", + "error": "Syntax Error: Invalid query syntax" + } + ] + } +}