diff --git a/src/cdk/v2/destinations/http/procWorkflow.yaml b/src/cdk/v2/destinations/http/procWorkflow.yaml
index e8696eb7a76..57483f31831 100644
--- a/src/cdk/v2/destinations/http/procWorkflow.yaml
+++ b/src/cdk/v2/destinations/http/procWorkflow.yaml
@@ -30,7 +30,8 @@ steps:
- name: deduceBodyFormat
template: |
- $.context.format = .destination.Config.format ?? 'JSON';
+ const format = .destination.Config.format ?? 'JSON';
+ $.context.format = $.CONTENT_TYPES_MAP[format];
- name: buildHeaders
template: |
@@ -45,8 +46,8 @@ steps:
- name: prepareParams
template: |
- const params = $.getCustomMappings(.message, .destination.Config.queryParams)
- $.context.params = $.encodeParamsObject(params)
+ const params = $.getCustomMappings(.message, .destination.Config.queryParams);
+ $.context.params = $.encodeParamsObject(params);
- name: deduceEndPoint
template: |
@@ -55,13 +56,12 @@ steps:
- name: prepareBody
template: |
const payload = $.getCustomMappings(.message, .destination.Config.propertiesMapping);
- $.context.payload = $.excludeMappedFields(payload, .destination.Config.propertiesMapping)
- $.context.format === "XML" && !$.isEmptyObject($.context.payload) ? $.context.payload = {payload: $.getXMLPayload($.context.payload)} : $.context.payload = $.removeUndefinedAndNullValues($.context.payload)
+ $.context.payload = $.prepareBody(payload, $.context.format);
- name: buildResponseForProcessTransformation
template: |
const response = $.defaultRequestConfig();
- $.context.format === "JSON" ? response.body.JSON = $.context.payload: response.body.XML = $.context.payload;
+ response.body[$.context.format] = $.context.payload;
response.endpoint = $.context.endpoint;
response.headers = $.context.headers;
response.method = $.context.method;
diff --git a/src/cdk/v2/destinations/http/utils.js b/src/cdk/v2/destinations/http/utils.js
index 6e53bbdc2f8..613e19d4531 100644
--- a/src/cdk/v2/destinations/http/utils.js
+++ b/src/cdk/v2/destinations/http/utils.js
@@ -8,9 +8,15 @@ const {
base64Convertor,
applyCustomMappings,
isEmptyObject,
- applyJSONStringTemplate,
+ removeUndefinedAndNullValues,
} = require('../../../../v0/util');
+const CONTENT_TYPES_MAP = {
+ JSON: 'JSON',
+ XML: 'XML',
+ FORM: 'FORM',
+};
+
const getAuthHeaders = (config) => {
let headers;
switch (config.auth) {
@@ -85,49 +91,14 @@ const getPathParamsSubString = (message, pathParamsArray) => {
};
const prepareEndpoint = (message, apiUrl, pathParams) => {
- let requestUrl;
- try {
- requestUrl = applyJSONStringTemplate(message, `\`${apiUrl}\``);
- } catch (e) {
- throw new ConfigurationError(`Error in api url template: ${e.message}`);
- }
if (!Array.isArray(pathParams)) {
- return requestUrl;
+ return apiUrl;
}
+ const requestUrl = apiUrl.replace(/\/{1,10}$/, '');
const pathParamsSubString = getPathParamsSubString(message, pathParams);
return `${requestUrl}${pathParamsSubString}`;
};
-const excludeMappedFields = (payload, mapping) => {
- const rawPayload = { ...payload };
- if (mapping) {
- mapping.forEach(({ from, to }) => {
- // continue when from === to
- if (from === to) return;
-
- // Remove the '$.' prefix and split the remaining string by '.'
- const keys = from.replace(/^\$\./, '').split('.');
- let current = rawPayload;
-
- // Traverse to the parent of the key to be removed
- keys.slice(0, -1).forEach((key) => {
- if (current?.[key]) {
- current = current[key];
- } else {
- current = null;
- }
- });
-
- if (current) {
- // Remove the 'from' field from input payload
- delete current[keys[keys.length - 1]];
- }
- });
- }
-
- return rawPayload;
-};
-
const sanitizeKey = (key) =>
key
.replace(/[^\w.-]/g, '_') // Replace invalid characters with underscores
@@ -157,9 +128,20 @@ const getXMLPayload = (payload) => {
const builderOptions = {
ignoreAttributes: false, // Include attributes if they exist
suppressEmptyNode: false, // Ensures that null or undefined values are not omitted
+ attributeNamePrefix: '@_',
};
+
+ if (Object.keys(payload).length !== 1) {
+ throw new ConfigurationError(
+ `Error: XML supports only one root key. Please update request body mappings accordingly`,
+ );
+ }
+ const rootKey = Object.keys(payload)[0];
+
const builder = new XMLBuilder(builderOptions);
- return `${builder.build(preprocessJson(payload))}`;
+ const processesPayload = preprocessJson(payload);
+ processesPayload[rootKey]['@_xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance';
+ return `${builder.build(processesPayload)}`;
};
const getMergedEvents = (batch) => {
@@ -172,10 +154,28 @@ const getMergedEvents = (batch) => {
return events;
};
-const metadataHeaders = (contentType) =>
- contentType === 'JSON'
- ? { 'Content-Type': 'application/json' }
- : { 'Content-Type': 'application/xml' };
+const metadataHeaders = (contentType) => {
+ switch (contentType) {
+ case CONTENT_TYPES_MAP.XML:
+ return { 'Content-Type': 'application/xml' };
+ case CONTENT_TYPES_MAP.FORM:
+ return { 'Content-Type': 'application/x-www-form-urlencoded' };
+ default:
+ return { 'Content-Type': 'application/json' };
+ }
+};
+
+const prepareBody = (payload, contentType) => {
+ let responseBody;
+ if (contentType === CONTENT_TYPES_MAP.XML && !isEmptyObject(payload)) {
+ responseBody = {
+ payload: getXMLPayload(payload),
+ };
+ } else {
+ responseBody = removeUndefinedAndNullValues(payload);
+ }
+ return responseBody;
+};
const mergeMetadata = (batch) => batch.map((event) => event.metadata[0]);
@@ -230,12 +230,12 @@ const batchSuccessfulEvents = (events, batchSize) => {
};
module.exports = {
+ CONTENT_TYPES_MAP,
getAuthHeaders,
getCustomMappings,
encodeParamsObject,
prepareEndpoint,
- excludeMappedFields,
- getXMLPayload,
metadataHeaders,
+ prepareBody,
batchSuccessfulEvents,
};
diff --git a/src/cdk/v2/destinations/http/utils.test.js b/src/cdk/v2/destinations/http/utils.test.js
index cf8e875974c..64fc87b66b4 100644
--- a/src/cdk/v2/destinations/http/utils.test.js
+++ b/src/cdk/v2/destinations/http/utils.test.js
@@ -1,4 +1,4 @@
-const { encodeParamsObject, prepareEndpoint, getXMLPayload } = require('./utils');
+const { encodeParamsObject, prepareEndpoint, prepareBody } = require('./utils');
const { XMLBuilder } = require('fast-xml-parser');
const jsonpath = require('rs-jsonpath');
@@ -21,12 +21,12 @@ describe('Utils Functions', () => {
describe('prepareEndpoint', () => {
test('should replace template variables in API URL', () => {
const message = { id: 123 };
- const apiUrl = 'https://api.example.com/resource/${$.id}';
- expect(prepareEndpoint(message, apiUrl, [])).toBe('https://api.example.com/resource/123');
+ const apiUrl = 'https://api.example.com/resource/';
+ expect(prepareEndpoint(message, apiUrl, [])).toBe('https://api.example.com/resource');
});
test('should replace template variables in API URL and add path params', () => {
const message = { id: 123, p2: 'P2' };
- const apiUrl = 'https://api.example.com/resource/${$.id}';
+ const apiUrl = 'https://api.example.com/resource/';
const pathParams = [
{
path: 'p1',
@@ -36,12 +36,12 @@ describe('Utils Functions', () => {
},
];
expect(prepareEndpoint(message, apiUrl, pathParams)).toBe(
- 'https://api.example.com/resource/123/p1/P2',
+ 'https://api.example.com/resource/p1/P2',
);
});
test('should add path params after uri encoding', () => {
const message = { id: 123, p2: 'P2%&' };
- const apiUrl = 'https://api.example.com/resource/${$.id}';
+ const apiUrl = 'https://api.example.com/resource/';
const pathParams = [
{
path: 'p1',
@@ -51,7 +51,7 @@ describe('Utils Functions', () => {
},
];
expect(prepareEndpoint(message, apiUrl, pathParams)).toBe(
- 'https://api.example.com/resource/123/p1/P2%25%26',
+ 'https://api.example.com/resource/p1/P2%25%26',
);
});
test('should throw error as path contains slash', () => {
@@ -71,12 +71,27 @@ describe('Utils Functions', () => {
});
});
- describe('getXMLPayload', () => {
- test('should generate XML payload with correct structure', () => {
- const payload = { key: null };
- const expectedXML = '';
- const result = getXMLPayload(payload);
- expect(result).toBe(expectedXML);
+ describe('prepareBody', () => {
+ test('should prepare XML payload when content type is XML', () => {
+ const payload = { root: { key: 'value', key2: null } };
+ const expectedXML =
+ 'value';
+ const result = prepareBody(payload, 'XML');
+ expect(result).toEqual({ payload: expectedXML });
+ });
+
+ test('should prepare FORM-URLENCODED payload when content type is FORM-URLENCODED', () => {
+ const payload = { key1: 'value1', key2: 'value2' };
+ const expectedFORM = { key1: 'value1', key2: 'value2' };
+ const result = prepareBody(payload, 'FORM-URLENCODED');
+ expect(result).toEqual(expectedFORM);
+ });
+
+ test('should return original payload without null or undefined values for other content types', () => {
+ const payload = { key1: 'value1', key2: null, key3: undefined, key4: 'value4' };
+ const expected = { key1: 'value1', key4: 'value4' };
+ const result = prepareBody(payload, 'JSON');
+ expect(result).toEqual(expected);
});
});
});
diff --git a/test/integrations/destinations/http/common.ts b/test/integrations/destinations/http/common.ts
index 8f4ca5d048f..4dc9d4daf27 100644
--- a/test/integrations/destinations/http/common.ts
+++ b/test/integrations/destinations/http/common.ts
@@ -92,13 +92,18 @@ const destinations: Destination[] = [
},
{
Config: {
- apiUrl: 'http://abc.com/contacts/{{$.traits.email}}/',
+ apiUrl: 'http://abc.com/contacts/',
auth: 'apiKeyAuth',
apiKeyName: 'x-api-key',
apiKeyValue: 'test-api-key',
method: 'DELETE',
isBatchingEnabled: true,
maxBatchSize: 4,
+ pathParams: [
+ {
+ path: '$.traits.email',
+ },
+ ],
},
DestinationDefinition: {
DisplayName: displayName,
@@ -114,13 +119,18 @@ const destinations: Destination[] = [
},
{
Config: {
- apiUrl: 'http://abc.com/contacts/{{$.traits.email}}/',
+ apiUrl: 'http://abc.com/contacts/',
auth: 'apiKeyAuth',
apiKeyName: 'x-api-key',
apiKeyValue: 'test-api-key',
method: 'GET',
isBatchingEnabled: true,
maxBatchSize: 4,
+ pathParams: [
+ {
+ path: '$.traits.email',
+ },
+ ],
},
DestinationDefinition: {
DisplayName: displayName,
@@ -158,27 +168,27 @@ const destinations: Destination[] = [
propertiesMapping: [
{
from: '$.event',
- to: '$.event',
+ to: '$.body.event',
},
{
from: '$.properties.currency',
- to: '$.currency',
+ to: '$.body.currency',
},
{
from: '$.userId',
- to: '$.userId',
+ to: '$.body.userId',
},
{
from: '$.properties.products[*].product_id',
- to: '$.properties.items[*].item_id',
+ to: '$.body.properties.items[*].item_id',
},
{
from: '$.properties.products[*].name',
- to: '$.properties.items[*].name',
+ to: '$.body.properties.items[*].name',
},
{
from: '$.properties.products[*].price',
- to: '$.properties.items[*].price',
+ to: '$.body.properties.items[*].price',
},
],
},
@@ -249,7 +259,7 @@ const destinations: Destination[] = [
},
{
Config: {
- apiUrl: 'http://abc.com/contacts/{{$.traits.phone}}',
+ apiUrl: 'http://abc.com/contacts/',
auth: 'noAuth',
method: 'POST',
format: 'JSON',
@@ -265,6 +275,11 @@ const destinations: Destination[] = [
from: '.traits.key',
},
],
+ pathParams: [
+ {
+ path: '$.traits.phone',
+ },
+ ],
},
DestinationDefinition: {
DisplayName: displayName,
@@ -436,6 +451,142 @@ const destinations: Destination[] = [
Transformations: [],
WorkspaceID: 'test-workspace-id',
},
+ {
+ Config: {
+ apiUrl: 'http://abc.com/events',
+ auth: 'bearerTokenAuth',
+ bearerToken: 'test-token',
+ method: 'POST',
+ format: 'FORM',
+ headers: [
+ {
+ to: '$.h1',
+ from: "'val1'",
+ },
+ {
+ to: '$.h2',
+ from: '$.key1',
+ },
+ {
+ to: "$.'content-type'",
+ from: "'application/json'",
+ },
+ ],
+ propertiesMapping: [
+ {
+ from: '$.event',
+ to: '$.event',
+ },
+ {
+ from: '$.properties.currency',
+ to: '$.currency',
+ },
+ {
+ from: '$.userId',
+ to: '$.userId',
+ },
+ ],
+ },
+ DestinationDefinition: {
+ DisplayName: displayName,
+ ID: '123',
+ Name: destTypeInUpperCase,
+ Config: { cdkV2Enabled: true },
+ },
+ Enabled: true,
+ ID: '123',
+ Name: destTypeInUpperCase,
+ Transformations: [],
+ WorkspaceID: 'test-workspace-id',
+ },
+ {
+ Config: {
+ apiUrl: 'http://abc.com/events',
+ auth: 'bearerTokenAuth',
+ bearerToken: 'test-token',
+ method: 'POST',
+ format: 'FORM',
+ headers: [
+ {
+ to: '$.h1',
+ from: "'val1'",
+ },
+ {
+ to: '$.h2',
+ from: '$.key1',
+ },
+ ],
+ propertiesMapping: [
+ {
+ from: '$.event',
+ to: '$.event',
+ },
+ {
+ from: '$.properties.currency',
+ to: '$.currency',
+ },
+ {
+ from: '$.userId',
+ to: '$.userId',
+ },
+ ],
+ },
+ DestinationDefinition: {
+ DisplayName: displayName,
+ ID: '123',
+ Name: destTypeInUpperCase,
+ Config: { cdkV2Enabled: true },
+ },
+ Enabled: true,
+ ID: '123',
+ Name: destTypeInUpperCase,
+ Transformations: [],
+ WorkspaceID: 'test-workspace-id',
+ },
+ {
+ Config: {
+ apiUrl: 'http://abc.com/events',
+ auth: 'bearerTokenAuth',
+ bearerToken: 'test-token',
+ method: 'POST',
+ format: 'FORM',
+ headers: [
+ {
+ to: '$.h1',
+ from: "'val1'",
+ },
+ {
+ to: '$.h2',
+ from: '$.key1',
+ },
+ ],
+ propertiesMapping: [
+ {
+ from: '$.event',
+ to: '$.event',
+ },
+ {
+ from: '$.properties.currency',
+ to: '$.currency',
+ },
+ {
+ from: '$.userId',
+ to: '$.userId',
+ },
+ ],
+ },
+ DestinationDefinition: {
+ DisplayName: displayName,
+ ID: '123',
+ Name: destTypeInUpperCase,
+ Config: { cdkV2Enabled: true },
+ },
+ Enabled: true,
+ ID: '123',
+ Name: destTypeInUpperCase,
+ Transformations: [],
+ WorkspaceID: 'test-workspace-id',
+ },
];
const traits = {
@@ -487,7 +638,7 @@ const properties = {
const processorInstrumentationErrorStatTags = {
destType: destTypeInUpperCase,
errorCategory: 'dataValidation',
- errorType: 'instrumentation',
+ errorType: 'configuration',
feature: 'processor',
implementation: 'cdkV2',
module: 'destination',
diff --git a/test/integrations/destinations/http/processor/configuration.ts b/test/integrations/destinations/http/processor/configuration.ts
index e2793fa90c3..54673bbae28 100644
--- a/test/integrations/destinations/http/processor/configuration.ts
+++ b/test/integrations/destinations/http/processor/configuration.ts
@@ -98,7 +98,7 @@ export const configuration: ProcessorTestData[] = [
output: transformResultBuilder({
method: 'DELETE',
userId: '',
- endpoint: 'http://abc.com/contacts/john.doe@example.com/',
+ endpoint: 'http://abc.com/contacts/john.doe%40example.com',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'test-api-key',
@@ -209,7 +209,7 @@ export const configuration: ProcessorTestData[] = [
},
XML: {
payload:
- 'Order CompletedUSDuserId123622c6f5d5cf86a4c77358033Cones of Dunshire40577c6f5d5cf86a4c7735ba03Five Crowns5',
+ '
Order CompletedUSDuserId123622c6f5d5cf86a4c77358033Cones of Dunshire40577c6f5d5cf86a4c7735ba03Five Crowns5',
},
}),
statusCode: 200,
@@ -328,9 +328,9 @@ export const configuration: ProcessorTestData[] = [
{
id: 'http-configuration-test-7',
name: destType,
- description: 'Track call with xml format and payload with special characters in keys',
+ description: 'Track call with xml format with multiple keys',
scenario: 'Business',
- successCriteria: 'Response should be in xml format with the special characters handled',
+ successCriteria: 'Should throw error as the body have multiple root keys',
feature: 'processor',
module: 'destination',
version: 'v0',
@@ -355,6 +355,49 @@ export const configuration: ProcessorTestData[] = [
method: 'POST',
},
},
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ error:
+ 'Error: XML supports only one root key. Please update request body mappings accordingly: Workflow: procWorkflow, Step: prepareBody, ChildStep: undefined, OriginalError: Error: XML supports only one root key. Please update request body mappings accordingly',
+ statusCode: 400,
+ metadata: generateMetadata(1),
+ statTags: { ...processorInstrumentationErrorStatTags },
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'http-configuration-test-8',
+ name: destType,
+ description:
+ 'Track call with bearer token, form format, post method, additional headers and properties mapping',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should be in form format with post method, headers and properties mapping',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ method: 'POST',
+ body: [
+ {
+ destination: destinations[10],
+ message: {
+ type: 'track',
+ userId: 'userId123',
+ event: 'Order Completed',
+ properties,
+ },
+ metadata: generateMetadata(1),
+ },
+ ],
+ },
+ },
output: {
response: {
status: 200,
@@ -363,17 +406,117 @@ export const configuration: ProcessorTestData[] = [
output: transformResultBuilder({
method: 'POST',
userId: '',
- endpoint: destinations[9].Config.apiUrl,
+ endpoint: destinations[10].Config.apiUrl,
headers: {
- 'Content-Type': 'application/xml',
+ 'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Bearer test-token',
h1: 'val1',
'content-type': 'application/json',
},
- XML: {
- payload:
- 'Rubik's Cube<_revenue-wdfqwe_>4.99',
+ FORM: {
+ currency: 'USD',
+ event: 'Order Completed',
+ userId: 'userId123',
+ },
+ }),
+ statusCode: 200,
+ metadata: generateMetadata(1),
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'http-configuration-test-9',
+ name: destType,
+ description: 'Track call with bearer token, form url encoded format',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should be in form format with post method, headers and properties mapping',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ method: 'POST',
+ body: [
+ {
+ destination: destinations[11],
+ message: {
+ type: 'track',
+ userId: 'userId123',
+ event: 'Order Completed',
+ properties,
+ },
+ metadata: generateMetadata(1),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ method: 'POST',
+ userId: '',
+ endpoint: destinations[11].Config.apiUrl,
+ headers: {
+ Authorization: 'Bearer test-token',
+ h1: 'val1',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ FORM: {
+ currency: 'USD',
+ event: 'Order Completed',
+ userId: 'userId123',
+ },
+ }),
+ statusCode: 200,
+ metadata: generateMetadata(1),
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'http-configuration-test-10',
+ name: destType,
+ description: 'empty body',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should be in form format with post method, headers and properties mapping',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ method: 'POST',
+ body: [
+ {
+ destination: destinations[12],
+ message: {},
+ metadata: generateMetadata(1),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ method: 'POST',
+ userId: '',
+ endpoint: destinations[12].Config.apiUrl,
+ headers: {
+ Authorization: 'Bearer test-token',
+ h1: 'val1',
+ 'Content-Type': 'application/x-www-form-urlencoded',
},
+ FORM: {},
}),
statusCode: 200,
metadata: generateMetadata(1),
diff --git a/test/integrations/destinations/http/router/data.ts b/test/integrations/destinations/http/router/data.ts
index 284a80f1090..8fd3a1584cf 100644
--- a/test/integrations/destinations/http/router/data.ts
+++ b/test/integrations/destinations/http/router/data.ts
@@ -182,7 +182,7 @@ export const data = [
version: '1',
type: 'REST',
method: 'GET',
- endpoint: 'http://abc.com/contacts/john.doe@example.com/',
+ endpoint: 'http://abc.com/contacts/john.doe%40example.com',
headers: {
'x-api-key': 'test-api-key',
'Content-Type': 'application/json',