fix: Preserve AWS request parameter values (#37303)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Robin Braumann 2026-08-28 13:04:42 +00:00 committed by GitHub
parent 3e6edc11b7
commit 267d98b637
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 326 additions and 85 deletions

View File

@ -867,7 +867,7 @@ export class AwsSes implements INodeType {
const params = [
'Action=DeleteCustomVerificationEmailTemplate',
`TemplateName=${templateName}`,
`TemplateName=${encodeURIComponent(templateName)}`,
];
responseData = await awsApiRequestSOAP.call(
@ -884,7 +884,7 @@ export class AwsSes implements INodeType {
if (operation === 'get') {
const templateName = this.getNodeParameter('templateName', i) as string;
const params = [`TemplateName=${templateName}`];
const params = [`TemplateName=${encodeURIComponent(templateName)}`];
responseData = await awsApiRequestSOAP.call(
this,
@ -925,7 +925,7 @@ export class AwsSes implements INodeType {
}
if (operation === 'send') {
const email = this.getNodeParameter('email', i) as string[];
const email = this.getNodeParameter('email', i) as string;
const templateName = this.getNodeParameter('templateName', i) as string;
@ -933,12 +933,14 @@ export class AwsSes implements INodeType {
const params = [
'Action=SendCustomVerificationEmail',
`TemplateName=${templateName}`,
`EmailAddress=${email}`,
`TemplateName=${encodeURIComponent(templateName)}`,
`EmailAddress=${encodeURIComponent(email)}`,
];
if (additionalFields.configurationSetName) {
params.push(`ConfigurationSetName=${additionalFields.configurationSetName}`);
params.push(
`ConfigurationSetName=${encodeURIComponent(additionalFields.configurationSetName as string)}`,
);
}
responseData = await awsApiRequestSOAP.call(
@ -959,27 +961,35 @@ export class AwsSes implements INodeType {
const params = [
'Action=UpdateCustomVerificationEmailTemplate',
`TemplateName=${templateName}`,
`TemplateName=${encodeURIComponent(templateName)}`,
];
if (updateFields.FailureRedirectionURL) {
params.push(`FailureRedirectionURL=${updateFields.FailureRedirectionURL}`);
params.push(
`FailureRedirectionURL=${encodeURIComponent(updateFields.FailureRedirectionURL as string)}`,
);
}
if (updateFields.email) {
params.push(`FromEmailAddress=${updateFields.email}`);
params.push(`FromEmailAddress=${encodeURIComponent(updateFields.email as string)}`);
}
if (updateFields.successRedirectionURL) {
params.push(`SuccessRedirectionURL=${updateFields.successRedirectionURL}`);
params.push(
`SuccessRedirectionURL=${encodeURIComponent(updateFields.successRedirectionURL as string)}`,
);
}
if (updateFields.templateContent) {
params.push(`TemplateContent=${updateFields.templateContent}`);
params.push(
`TemplateContent=${encodeURIComponent(updateFields.templateContent as string)}`,
);
}
if (updateFields.templateSubject) {
params.push(`TemplateSubject=${updateFields.templateSubject}`);
params.push(
`TemplateSubject=${encodeURIComponent(updateFields.templateSubject as string)}`,
);
}
responseData = await awsApiRequestSOAP.call(
@ -1031,19 +1041,25 @@ export class AwsSes implements INodeType {
}
if (additionalFields.configurationSetName) {
params.push(`ConfigurationSetName=${additionalFields.configurationSetName}`);
params.push(
`ConfigurationSetName=${encodeURIComponent(additionalFields.configurationSetName as string)}`,
);
}
if (additionalFields.returnPath) {
params.push(`ReturnPath=${additionalFields.returnPath}`);
params.push(
`ReturnPath=${encodeURIComponent(additionalFields.returnPath as string)}`,
);
}
if (additionalFields.returnPathArn) {
params.push(`ReturnPathArn=${additionalFields.returnPathArn}`);
params.push(
`ReturnPathArn=${encodeURIComponent(additionalFields.returnPathArn as string)}`,
);
}
if (additionalFields.sourceArn) {
params.push(`SourceArn=${additionalFields.sourceArn}`);
params.push(`SourceArn=${encodeURIComponent(additionalFields.sourceArn as string)}`);
}
if (additionalFields.replyToAddresses) {
@ -1083,7 +1099,10 @@ export class AwsSes implements INodeType {
const fromEmail = this.getNodeParameter('fromEmail', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const templateDataUi = this.getNodeParameter('templateDataUi', i) as IDataObject;
const params = [`Template=${template}`, `Source=${encodeURIComponent(fromEmail)}`];
const params = [
`Template=${encodeURIComponent(template)}`,
`Source=${encodeURIComponent(fromEmail)}`,
];
if (toAddresses.length) {
setParameter(params, 'Destination.ToAddresses.member', toAddresses);
@ -1096,19 +1115,25 @@ export class AwsSes implements INodeType {
}
if (additionalFields.configurationSetName) {
params.push(`ConfigurationSetName=${additionalFields.configurationSetName}`);
params.push(
`ConfigurationSetName=${encodeURIComponent(additionalFields.configurationSetName as string)}`,
);
}
if (additionalFields.returnPath) {
params.push(`ReturnPath=${additionalFields.returnPath}`);
params.push(
`ReturnPath=${encodeURIComponent(additionalFields.returnPath as string)}`,
);
}
if (additionalFields.returnPathArn) {
params.push(`ReturnPathArn=${additionalFields.returnPathArn}`);
params.push(
`ReturnPathArn=${encodeURIComponent(additionalFields.returnPathArn as string)}`,
);
}
if (additionalFields.sourceArn) {
params.push(`SourceArn=${additionalFields.sourceArn}`);
params.push(`SourceArn=${encodeURIComponent(additionalFields.sourceArn as string)}`);
}
if (additionalFields.replyToAddresses) {
@ -1193,7 +1218,7 @@ export class AwsSes implements INodeType {
if (operation === 'delete') {
const templateName = this.getNodeParameter('templateName', i) as string;
const params = [`TemplateName=${templateName}`];
const params = [`TemplateName=${encodeURIComponent(templateName)}`];
responseData = await awsApiRequestSOAP.call(
this,
@ -1208,7 +1233,7 @@ export class AwsSes implements INodeType {
if (operation === 'get') {
const templateName = this.getNodeParameter('templateName', i) as string;
const params = [`TemplateName=${templateName}`];
const params = [`TemplateName=${encodeURIComponent(templateName)}`];
responseData = await awsApiRequestSOAP.call(
this,

View File

@ -1,5 +1,5 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import { NodeConnectionTypes, type WorkflowTestData } from 'n8n-workflow';
import { NodeConnectionTypes, type INodeParameters, type WorkflowTestData } from 'n8n-workflow';
import assert from 'node:assert';
import qs from 'node:querystring';
@ -8,9 +8,55 @@ import { credentials } from '../../__tests__/credentials';
describe('AwsSes Node', () => {
const testHarness = new NodeTestHarness();
const email = 'test+user@example.com';
const configurationSetName = 'config&segment=value';
const returnPath = 'bounce+tag@example.com';
const returnPathArn = 'arn:aws:ses:eu-central-1:123456789012:identity/bounce+tag@example.com';
const sourceArn = 'arn:aws:ses:eu-central-1:123456789012:identity/example.com&segment=value';
const templateName = 'Template&segment=value';
const templateData = {
Name: 'Special. Characters @#$%^&*()_-',
};
const createWorkflowData = (
parameters: INodeParameters,
): WorkflowTestData['input']['workflowData'] => ({
nodes: [
{
parameters: {},
id: 'b30ae9d4-6a92-4b62-92f4-5810b4718c66',
name: 'When clicking ‘Execute workflow’',
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [720, 380],
},
{
parameters,
id: '07955ca4-e9c9-415a-8175-dda8ad3204fd',
name: 'AWS SES',
type: 'n8n-nodes-base.awsSes',
typeVersion: 1,
position: [940, 380],
credentials: {
aws: {
id: '1',
name: 'AWS',
},
},
},
],
connections: {
'When clicking ‘Execute workflow’': {
main: [
[
{
node: 'AWS SES',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
});
const tests: WorkflowTestData[] = [
{
description: 'should create customVerificationEmail',
@ -94,61 +140,28 @@ describe('AwsSes Node', () => {
},
},
{
description: 'should URIencode params for sending email with template',
description: 'should preserve reserved characters when sending email with a template',
input: {
workflowData: {
nodes: [
{
parameters: {},
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [-180, 520],
id: '363e874a-9054-4a64-bc3f-786719dde626',
name: 'When clicking ‘Execute workflow’',
},
{
parameters: {
operation: 'sendTemplate',
templateName: '=Template11',
fromEmail: 'test+user@example.com',
toAddresses: ['test+user@example.com'],
templateDataUi: {
templateDataValues: [
{
key: 'Name',
value: '=Special. Characters @#$%^&*()_-',
},
],
},
additionalFields: {},
workflowData: createWorkflowData({
operation: 'sendTemplate',
templateName,
fromEmail: email,
toAddresses: [email],
templateDataUi: {
templateDataValues: [
{
key: 'Name',
value: templateData.Name,
},
type: 'n8n-nodes-base.awsSes',
typeVersion: 1,
position: [60, 520],
id: '13bbf4ef-8320-45d1-9210-61b62794a108',
name: 'AWS SES',
credentials: {
aws: {
id: 'Nz0QZhzu3MvfK4TQ',
name: 'AWS account',
},
},
},
],
connections: {
'When clicking ‘Execute workflow’': {
main: [
[
{
node: 'AWS SES',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
],
},
},
additionalFields: {
configurationSetName,
returnPath,
returnPathArn,
sourceArn,
},
}),
},
output: {
nodeData: { 'AWS SES': [[{ json: { success: 'true' } }]] },
@ -158,7 +171,7 @@ describe('AwsSes Node', () => {
mocks: [
{
method: 'post',
path: `/?Action=SendTemplatedEmail&Template=Template11&Source=${encodeURIComponent(email)}&Destination.ToAddresses.member.1=${encodeURIComponent(email)}&TemplateData=${encodeURIComponent(JSON.stringify(templateData))}`,
path: `/?Action=SendTemplatedEmail&Template=${encodeURIComponent(templateName)}&Source=${encodeURIComponent(email)}&Destination.ToAddresses.member.1=${encodeURIComponent(email)}&ConfigurationSetName=${encodeURIComponent(configurationSetName)}&ReturnPath=${encodeURIComponent(returnPath)}&ReturnPathArn=${encodeURIComponent(returnPathArn)}&SourceArn=${encodeURIComponent(sourceArn)}&TemplateData=${encodeURIComponent(JSON.stringify(templateData))}`,
statusCode: 200,
responseBody:
'<SendTemplatedEmailResponse><success>true</success></SendTemplatedEmailResponse>',
@ -166,6 +179,104 @@ describe('AwsSes Node', () => {
],
},
},
{
description: 'should preserve reserved characters when sending an email',
input: {
workflowData: createWorkflowData({
resource: 'email',
operation: 'send',
fromEmail: email,
toAddresses: [email],
subject: 'Test subject',
body: 'Test body',
isBodyHtml: false,
additionalFields: {
configurationSetName,
returnPath,
returnPathArn,
sourceArn,
},
}),
},
output: {
nodeData: {
'AWS SES': [[{ json: { SendEmailResponse: { success: 'true' } } }]],
},
},
nock: {
baseUrl: 'https://email.eu-central-1.amazonaws.com',
mocks: [
{
method: 'post',
path: `/?Action=SendEmail&Message.Subject.Data=Test%20subject&Source=${encodeURIComponent(email)}&Message.Body.Text.Data=Test%20body&Destination.ToAddresses.member.1=${encodeURIComponent(email)}&ConfigurationSetName=${encodeURIComponent(configurationSetName)}&ReturnPath=${encodeURIComponent(returnPath)}&ReturnPathArn=${encodeURIComponent(returnPathArn)}&SourceArn=${encodeURIComponent(sourceArn)}`,
statusCode: 200,
responseBody: '<SendEmailResponse><success>true</success></SendEmailResponse>',
},
],
},
},
{
description: 'should preserve reserved characters when getting a template',
input: {
workflowData: createWorkflowData({
resource: 'template',
operation: 'get',
templateName,
}),
},
output: {
nodeData: { 'AWS SES': [[{ json: { success: 'true' } }]] },
},
nock: {
baseUrl: 'https://email.eu-central-1.amazonaws.com',
mocks: [
{
method: 'post',
path: `/?Action=GetTemplate&TemplateName=${encodeURIComponent(templateName)}`,
statusCode: 200,
responseBody: '<GetTemplateResponse><success>true</success></GetTemplateResponse>',
},
],
},
},
{
description: 'should preserve reserved characters when sending a custom verification email',
input: {
workflowData: createWorkflowData({
resource: 'customVerificationEmail',
operation: 'send',
email,
templateName,
additionalFields: {
configurationSetName,
},
}),
},
output: {
nodeData: { 'AWS SES': [[{ json: { success: 'true' } }]] },
},
nock: {
baseUrl: 'https://email.eu-central-1.amazonaws.com',
mocks: [
{
method: 'post',
path: '/',
requestBody: (body: string) => {
assert.deepEqual(qs.parse(body), {
Action: 'SendCustomVerificationEmail',
TemplateName: templateName,
EmailAddress: email,
ConfigurationSetName: configurationSetName,
});
return true;
},
statusCode: 200,
responseBody:
'<SendCustomVerificationEmailResponse><success>true</success></SendCustomVerificationEmailResponse>',
},
],
},
},
];
for (const testData of tests) {

View File

@ -316,12 +316,12 @@ export class AwsSqs implements INodeType {
'',
) as string;
if (messageDeduplicationId) {
params.push(`MessageDeduplicationId=${messageDeduplicationId}`);
params.push(`MessageDeduplicationId=${encodeURIComponent(messageDeduplicationId)}`);
}
const messageGroupId = this.getNodeParameter('messageGroupId', i) as string;
if (messageGroupId) {
params.push(`MessageGroupId=${messageGroupId}`);
params.push(`MessageGroupId=${encodeURIComponent(messageGroupId)}`);
}
}
@ -331,8 +331,12 @@ export class AwsSqs implements INodeType {
this.getNodeParameter('options.messageAttributes.string', i, []) as INodeParameters[]
).forEach((attribute) => {
attributeCount++;
params.push(`MessageAttribute.${attributeCount}.Name=${attribute.name}`);
params.push(`MessageAttribute.${attributeCount}.Value.StringValue=${attribute.value}`);
params.push(
`MessageAttribute.${attributeCount}.Name=${encodeURIComponent(attribute.name as string)}`,
);
params.push(
`MessageAttribute.${attributeCount}.Value.StringValue=${encodeURIComponent(attribute.value as string)}`,
);
params.push(`MessageAttribute.${attributeCount}.Value.DataType=String`);
});
@ -345,8 +349,12 @@ export class AwsSqs implements INodeType {
const dataPropertyName = attribute.dataPropertyName as string;
const binaryData = this.helpers.assertBinaryData(i, dataPropertyName);
params.push(`MessageAttribute.${attributeCount}.Name=${attribute.name}`);
params.push(`MessageAttribute.${attributeCount}.Value.BinaryValue=${binaryData.data}`);
params.push(
`MessageAttribute.${attributeCount}.Name=${encodeURIComponent(attribute.name as string)}`,
);
params.push(
`MessageAttribute.${attributeCount}.Value.BinaryValue=${encodeURIComponent(binaryData.data)}`,
);
params.push(`MessageAttribute.${attributeCount}.Value.DataType=Binary`);
});
@ -355,8 +363,12 @@ export class AwsSqs implements INodeType {
this.getNodeParameter('options.messageAttributes.number', i, []) as INodeParameters[]
).forEach((attribute) => {
attributeCount++;
params.push(`MessageAttribute.${attributeCount}.Name=${attribute.name}`);
params.push(`MessageAttribute.${attributeCount}.Value.StringValue=${attribute.value}`);
params.push(
`MessageAttribute.${attributeCount}.Name=${encodeURIComponent(attribute.name as string)}`,
);
params.push(
`MessageAttribute.${attributeCount}.Value.StringValue=${encodeURIComponent(attribute.value as number)}`,
);
params.push(`MessageAttribute.${attributeCount}.Value.DataType=Number`);
});

View File

@ -0,0 +1,93 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import { NodeConnectionTypes, type WorkflowTestData } from 'n8n-workflow';
import { credentials } from '../../__tests__/credentials';
describe('AwsSqs Node', () => {
const messageAttributeName = 'label&kind=primary';
const messageAttributeValue = 'left&segment=value';
const messageDeduplicationId = 'dedup&segment=value';
const messageGroupId = 'group&segment=value';
const queuePath = '/123456789012/n8n-node-test-fifo.fifo';
const testData: WorkflowTestData = {
description: 'should preserve reserved characters in request parameter values',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '5d4c45ed-1368-4ea3-a97c-2d46664f4656',
name: 'When clicking ‘Execute workflow’',
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [720, 380],
},
{
parameters: {
queue: `https://sqs.eu-central-1.amazonaws.com${queuePath}`,
queueType: 'fifo',
sendInputData: false,
message: 'test message',
messageGroupId,
options: {
messageDeduplicationId,
messageAttributes: {
string: [
{
name: messageAttributeName,
value: messageAttributeValue,
},
],
},
},
},
id: 'bf664a84-bd26-413b-94d3-7f2935883ce3',
name: 'AWS SQS',
type: 'n8n-nodes-base.awsSqs',
typeVersion: 1,
position: [940, 380],
credentials: {
aws: {
id: '1',
name: 'AWS',
},
},
},
],
connections: {
'When clicking ‘Execute workflow’': {
main: [
[
{
node: 'AWS SQS',
type: NodeConnectionTypes.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeData: {
'AWS SQS': [[{ json: { MessageId: 'message-id' } }]],
},
},
nock: {
baseUrl: 'https://sqs.eu-central-1.amazonaws.com',
mocks: [
{
method: 'get',
path: `${queuePath}?Version=2012-11-05&Action=SendMessage&MessageBody=test%20message&MessageDeduplicationId=${encodeURIComponent(messageDeduplicationId)}&MessageGroupId=${encodeURIComponent(messageGroupId)}&MessageAttribute.1.Name=${encodeURIComponent(messageAttributeName)}&MessageAttribute.1.Value.StringValue=${encodeURIComponent(messageAttributeValue)}&MessageAttribute.1.Value.DataType=String`,
statusCode: 200,
responseBody:
'<SendMessageResponse><SendMessageResult><MessageId>message-id</MessageId></SendMessageResult></SendMessageResponse>',
},
],
},
};
new NodeTestHarness().setupTest(testData, { credentials });
});