import * as express from 'express' ; import { get } from 'lodash' ; import { BINARY_ENCODING , BinaryDataManager , NodeExecuteFunctions } from 'n8n-core' ; import { createDeferredPromise , IBinaryKeyData , IDataObject , IDeferredPromise , IExecuteData , IExecuteResponsePromiseData , IN8nHttpFullResponse , INode , IRunExecutionData , IWebhookData , IWebhookResponseData , IWorkflowDataProxyAdditionalKeys , IWorkflowExecuteAdditionalData , LoggerProxy as Logger , NodeHelpers , Workflow , WorkflowExecuteMode , } from 'n8n-workflow' ; import { GenericHelpers , IExecutionDb , IResponseCallbackData , IWorkflowDb , IWorkflowExecutionDataProcess , ResponseHelper , WorkflowExecuteAdditionalData , WorkflowHelpers , WorkflowRunner , } from '.' ; import * as ActiveExecutions from './ActiveExecutions' ; const activeExecutions = ActiveExecutions . getInstance ( ) ; export param { string } workflowId param { Workflow } workflow returns { IWebhookData [ ] } export function getWorkflowWebhooks ( workflow , additionalData , destinationNode ? , ignoreRestartWehbooks = false ) { const returnData = [ ] ; let parentNodes ; if ( destinationNode !== undefined ) { parentNodes = workflow . getParentNodes ( destinationNode ) ; parentNodes . push ( destinationNode ) ; } for ( const node of Object . values ( workflow . nodes ) ) { if ( parentNodes !== undefined && ! parentNodes . includes ( node . name ) ) { continue ; } returnData . push . apply ( returnData , NodeHelpers . getNodeWebhooks ( workflow , node , additionalData , ignoreRestartWehbooks ) ) ; } return returnData ; } export function decodeWebhookResponse ( response ) { if ( typeof response === 'object' && typeof response . body === 'object' && ( response . body as IDataObject ) [ '__@N8nEncodedBuffer@__' ] ) { response . body = Buffer . from ( ( response . body as IDataObject ) [ '__@N8nEncodedBuffer@__' ] as string , BINARY_ENCODING ) ; } return response ; } export function encodeWebhookResponse ( response ) { if ( typeof response === 'object' && Buffer . isBuffer ( response . body ) ) { response . body = { '__@N8nEncodedBuffer@__' : response . body . toString ( BINARY_ENCODING ) , } ; } return response ; } export param { string } workflowId param { Workflow } workflow returns { IWebhookData [ ] } export function getWorkflowWebhooksBasic ( workflow ) { const returnData = [ ] ; for ( const node of Object . values ( workflow . nodes ) ) { returnData . push . apply ( returnData , NodeHelpers . getNodeWebhooksBasic ( workflow , node ) ) ; } return returnData ; } export param { IWebhookData } webhookData param { IWorkflowDb } workflowData param { INode } workflowStartNode param { WorkflowExecuteMode } executionMode param { ( string | undefined ) } sessionId param { express . Request } req param { express . Response } res param { ( ( error : Error | null , data : IResponseCallbackData ) => void ) } responseCallback returns { ( Promise < string | undefined > ) } export async function executeWebhook ( workflow , webhookData , workflowData , workflowStartNode , executionMode , sessionId , runExecutionData , executionId , req , res , responseCallback ) { const nodeType = workflow . nodeTypes . getByNameAndVersion ( workflowStartNode . type , workflowStartNode . typeVersion ) ; if ( nodeType === undefined ) { const errorMessage = `The type of the webhook node "${ workflowStartNode . name }" is not known.` ; responseCallback ( new Error ( errorMessage ) , { } ) ; throw new ResponseHelper . ResponseError ( errorMessage , 500 , 500 ) ; } const additionalKeys = { $executionId : executionId , } ; const responseMode = workflow . expression . getSimpleParameterValue ( workflowStartNode , webhookData . webhookDescription . responseMode , executionMode , additionalKeys , 'onReceived' ) ; const responseCode = workflow . expression . getSimpleParameterValue ( workflowStartNode , webhookData . webhookDescription . responseCode , executionMode , additionalKeys , 200 ) as number ; if ( ! [ 'onReceived' , 'lastNode' , 'responseNode' ] . includes ( responseMode as string ) ) { const errorMessage = `The response mode ${ responseMode } is not valid!` ; responseCallback ( new Error ( errorMessage ) , { } ) ; throw new ResponseHelper . ResponseError ( errorMessage , 500 , 500 ) ; } const additionalData = await WorkflowExecuteAdditionalData . getBase ( ) ; additionalData . httpRequest = req ; additionalData . httpResponse = res ; let didSendResponse = false ; let runExecutionDataMerge = { } ; try { let webhookResultData ; try { webhookResultData = await workflow . runWebhook ( webhookData , workflowStartNode , additionalData , NodeExecuteFunctions , executionMode ) ; } catch ( err ) { const errorMessage = 'Workflow Webhook Error: Workflow could not be started!' ; responseCallback ( new Error ( errorMessage ) , { } ) ; didSendResponse = true ; runExecutionDataMerge = { resultData : { runData : { } , lastNodeExecuted : workflowStartNode . name , error : { ... err , message : err . message , stack : err . stack , } , } , } ; webhookResultData = { noWebhookResponse : true , workflowData : [ [ { json : { } } ] ] , } ; } await WorkflowHelpers . saveStaticData ( workflow ) ; const additionalKeys = { $executionId : executionId , } ; if ( webhookData . webhookDescription . responseHeaders !== undefined ) { const responseHeaders = workflow . expression . getComplexParameterValue ( workflowStartNode , webhookData . webhookDescription . responseHeaders , executionMode , additionalKeys , undefined ) as { entries ? : Array < { name : string ; value : string ; } > | undefined ; } ; if ( responseHeaders !== undefined && responseHeaders . entries !== undefined ) { for ( const item of responseHeaders . entries ) { res . setHeader ( item . name , item . value ) ; } } } if ( webhookResultData . noWebhookResponse === true && ! didSendResponse ) { responseCallback ( null , { noWebhookResponse : true , } ) ; didSendResponse = true ; } if ( webhookResultData . workflowData === undefined ) { if ( webhookResultData . webhookResponse !== undefined ) { if ( ! didSendResponse ) { responseCallback ( null , { data : webhookResultData . webhookResponse , responseCode , } ) ; didSendResponse = true ; } } else { if ( ! didSendResponse ) { responseCallback ( null , { data : { message : 'Webhook call got received.' , } , responseCode , } ) ; didSendResponse = true ; } } return ; } if ( responseMode === 'onReceived' && ! didSendResponse ) { if ( webhookResultData . webhookResponse !== undefined ) { responseCallback ( null , { data : webhookResultData . webhookResponse , responseCode , } ) ; } else { responseCallback ( null , { data : { message : 'Workflow got started.' , } , responseCode , } ) ; } didSendResponse = true ; } const nodeExecutionStack = [ ] ; nodeExecutionStack . push ( { node : workflowStartNode , data : { main : webhookResultData . workflowData , } , } ) ; runExecutionData = runExecutionData || ( { startData : { } , resultData : { runData : { } , } , executionData : { contextData : { } , nodeExecutionStack , waitingExecution : { } , } , } as IRunExecutionData ) ; if ( executionId !== undefined ) { runExecutionData . executionData ! . nodeExecutionStack [ 0 ] . data . main = webhookResultData . workflowData ; } if ( Object . keys ( runExecutionDataMerge ) . length !== 0 ) { Object . assign ( runExecutionData , runExecutionDataMerge ) ; } const runData = { executionMode , executionData : runExecutionData , sessionId , workflowData , } ; let responsePromise ; if ( responseMode === 'responseNode' ) { responsePromise = await createDeferredPromise < IN8nHttpFullResponse > ( ) ; responsePromise . promise ( ) . then ( ( response ) => { if ( didSendResponse ) { return ; } if ( Buffer . isBuffer ( response . body ) ) { res . header ( response . headers ) ; res . end ( response . body ) ; responseCallback ( null , { noWebhookResponse : true , } ) ; } else { responseCallback ( null , { data : response . body as IDataObject , headers : response . headers , responseCode : response . statusCode , } ) ; } didSendResponse = true ; } ) . catch ( async ( error ) => { Logger . error ( `Error with Webhook-Response for execution "${ executionId }": "${ error . message }"` , { executionId , workflowId : workflow . id } ) ; } ) ; } const workflowRunner = new WorkflowRunner ( ) ; executionId = await workflowRunner . run ( runData , true , ! didSendResponse , executionId , responsePromise ) ; Logger . verbose ( `Started execution of workflow "${ workflow . name }" from webhook with execution ID ${ executionId }` , { executionId } ) ; const executePromise = activeExecutions . getPostExecutePromise ( executionId ) as Promise < IExecutionDb | undefined > ; executePromise . then ( async ( data ) => { if ( data === undefined ) { if ( ! didSendResponse ) { responseCallback ( null , { data : { message : 'Workflow did execute sucessfully but no data got returned.' , } , responseCode , } ) ; didSendResponse = true ; } return undefined ; } const returnData = WorkflowHelpers . getDataLastExecutedNodeData ( data ) ; if ( data . data . resultData . error || returnData ?. error !== undefined ) { if ( ! didSendResponse ) { responseCallback ( null , { data : { message : 'Workflow did error.' , } , responseCode : 500 , } ) ; } didSendResponse = true ; return data ; } if ( responseMode === 'responseNode' ) { if ( ! didSendResponse ) { responseCallback ( null , { data : { message : 'Workflow executed sucessfully.' , } , responseCode , } ) ; didSendResponse = true ; } return undefined ; } if ( returnData === undefined ) { if ( ! didSendResponse ) { responseCallback ( null , { data : { message : 'Workflow did execute sucessfully but the last node did not return any data.' , } , responseCode , } ) ; } didSendResponse = true ; return data ; } const additionalKeys = { $executionId : executionId , } ; const responseData = workflow . expression . getSimpleParameterValue ( workflowStartNode , webhookData . webhookDescription . responseData , executionMode , additionalKeys , 'firstEntryJson' ) ; if ( ! didSendResponse ) { let data ; if ( responseData === 'firstEntryJson' ) { if ( returnData . data ! . main [ 0 ] ! [ 0 ] === undefined ) { responseCallback ( new Error ( 'No item to return got found.' ) , { } ) ; didSendResponse = true ; } data = returnData . data ! . main [ 0 ] ! [ 0 ] . json ; const responsePropertyName = workflow . expression . getSimpleParameterValue ( workflowStartNode , webhookData . webhookDescription . responsePropertyName , executionMode , additionalKeys , undefined ) ; if ( responsePropertyName !== undefined ) { data = get ( data , responsePropertyName as string ) as IDataObject ; } const responseContentType = workflow . expression . getSimpleParameterValue ( workflowStartNode , webhookData . webhookDescription . responseContentType , executionMode , additionalKeys , undefined ) ; if ( responseContentType !== undefined ) { res . setHeader ( 'Content-Type' , responseContentType as string ) ; if ( data !== null && data !== undefined && [ 'Buffer' , 'String' ] . includes ( data . constructor . name ) ) { res . end ( data ) ; } else { res . end ( JSON . stringify ( data ) ) ; } responseCallback ( null , { noWebhookResponse : true , } ) ; didSendResponse = true ; } } else if ( responseData === 'firstEntryBinary' ) { data = returnData . data ! . main [ 0 ] ! [ 0 ] ; if ( data === undefined ) { responseCallback ( new Error ( 'No item to return got found.' ) , { } ) ; didSendResponse = true ; } if ( data . binary === undefined ) { responseCallback ( new Error ( 'No binary data to return got found.' ) , { } ) ; didSendResponse = true ; } const responseBinaryPropertyName = workflow . expression . getSimpleParameterValue ( workflowStartNode , webhookData . webhookDescription . responseBinaryPropertyName , executionMode , additionalKeys , 'data' ) ; if ( responseBinaryPropertyName === undefined && ! didSendResponse ) { responseCallback ( new Error ( 'No "responseBinaryPropertyName" is set.' ) , { } ) ; didSendResponse = true ; } const binaryData = ( data . binary as IBinaryKeyData ) [ responseBinaryPropertyName as string ] ; if ( binaryData === undefined && ! didSendResponse ) { responseCallback ( new Error ( `The binary property "${ responseBinaryPropertyName }" which should be returned does not exist.` ) , { } ) ; didSendResponse = true ; } if ( ! didSendResponse ) { res . setHeader ( 'Content-Type' , binaryData . mimeType ) ; const binaryDataBuffer = await BinaryDataManager . getInstance ( ) . retrieveBinaryData ( binaryData ) ; res . end ( binaryDataBuffer ) ; responseCallback ( null , { noWebhookResponse : true , } ) ; } } else { data = [ ] ; for ( const entry of returnData . data ! . main [ 0 ] ! ) { data . push ( entry . json ) ; } } if ( ! didSendResponse ) { responseCallback ( null , { data , responseCode , } ) ; } } didSendResponse = true ; return data ; } ) . catch ( ( e ) => { if ( ! didSendResponse ) { responseCallback ( new Error ( 'There was a problem executing the workflow.' ) , { } ) ; } throw new ResponseHelper . ResponseError ( e . message , 500 , 500 ) ; } ) ; return executionId ; } catch ( e ) { if ( ! didSendResponse ) { responseCallback ( new Error ( 'There was a problem executing the workflow.' ) , { } ) ; } throw new ResponseHelper . ResponseError ( e . message , 500 , 500 ) ; } } export returns export function getWebhookBaseUrl ( ) { let urlBaseWebhook = GenericHelpers . getBaseUrl ( ) ; if ( process . env . WEBHOOK_TUNNEL_URL !== undefined || process . env . WEBHOOK_URL !== undefined ) { urlBaseWebhook = process . env . WEBHOOK_TUNNEL_URL || process . env . WEBHOOK_URL ; } if ( ! urlBaseWebhook . endsWith ( '/' ) ) { urlBaseWebhook += '/' ; } return urlBaseWebhook ; }