mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 11:01:15 +00:00
fix(editor): Import form data with special characters from curl command correctly (#14898)
This commit is contained in:
@@ -1,6 +1,25 @@
|
|||||||
import { toHttpNodeParameters } from '@/composables/useImportCurlCommand';
|
import { toHttpNodeParameters, useImportCurlCommand } from '@/composables/useImportCurlCommand';
|
||||||
|
|
||||||
|
const showToast = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('@/composables/useToast', () => ({
|
||||||
|
useToast: () => ({ showToast }),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('useImportCurlCommand', () => {
|
describe('useImportCurlCommand', () => {
|
||||||
|
describe('importCurlCommand', () => {
|
||||||
|
test('Should parse cURL command with invalid protocol', () => {
|
||||||
|
const curl = 'curl ftp://reqbin.com/echo -X POST';
|
||||||
|
useImportCurlCommand().importCurlCommand(curl);
|
||||||
|
expect(showToast).toHaveBeenCalledWith({
|
||||||
|
duration: 0,
|
||||||
|
message: 'The HTTP node doesn’t support FTP requests',
|
||||||
|
title: 'Use the FTP node',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('toHttpNodeParameters', () => {
|
describe('toHttpNodeParameters', () => {
|
||||||
test('Should parse form-urlencoded content type correctly', () => {
|
test('Should parse form-urlencoded content type correctly', () => {
|
||||||
const curl =
|
const curl =
|
||||||
@@ -292,5 +311,254 @@ describe('useImportCurlCommand', () => {
|
|||||||
expect(parameters.sendBody).toBe(false);
|
expect(parameters.sendBody).toBe(false);
|
||||||
expect(parameters.options.allowUnauthorizedCerts).toBe(true);
|
expect(parameters.options.allowUnauthorizedCerts).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Should parse form url encoded body data if parameter has base64 special characters like / or % or =', () => {
|
||||||
|
const curl =
|
||||||
|
'curl -X POST https://reqbin.com/echo \
|
||||||
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||||
|
-d "redirect_uri=https://test.app.n8n.cloud/webhook-test/12345" \
|
||||||
|
-d "client_secret=secret%3D%3D"';
|
||||||
|
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('POST');
|
||||||
|
expect(parameters.sendBody).toBe(true);
|
||||||
|
expect(parameters.contentType).toEqual('form-urlencoded');
|
||||||
|
expect(parameters.bodyParameters).toEqual({
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'redirect_uri',
|
||||||
|
value: 'https://test.app.n8n.cloud/webhook-test/12345',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'client_secret',
|
||||||
|
value: 'secret==',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with no headers, body, or query parameters', () => {
|
||||||
|
const curl = 'curl https://reqbin.com/echo';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('GET');
|
||||||
|
expect(parameters.sendBody).toBe(false);
|
||||||
|
expect(parameters.contentType).toBeUndefined();
|
||||||
|
expect(parameters.sendHeaders).toBe(false);
|
||||||
|
expect(parameters.sendQuery).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with custom HTTP method', () => {
|
||||||
|
const curl = 'curl -X DELETE https://reqbin.com/echo';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('DELETE');
|
||||||
|
expect(parameters.sendBody).toBe(false);
|
||||||
|
expect(parameters.contentType).toBeUndefined();
|
||||||
|
expect(parameters.sendHeaders).toBe(false);
|
||||||
|
expect(parameters.sendQuery).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with multiple headers', () => {
|
||||||
|
const curl =
|
||||||
|
'curl https://reqbin.com/echo -H "Authorization: Bearer token" -H "Accept: application/json"';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.sendBody).toBe(false);
|
||||||
|
expect(parameters.contentType).toBeUndefined();
|
||||||
|
expect(parameters.sendHeaders).toBe(true);
|
||||||
|
expect(parameters.headerParameters?.parameters[0].name).toBe('Authorization');
|
||||||
|
expect(parameters.headerParameters?.parameters[0].value).toBe('Bearer token');
|
||||||
|
expect(parameters.headerParameters?.parameters[1].name).toBe('Accept');
|
||||||
|
expect(parameters.headerParameters?.parameters[1].value).toBe('application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with query parameters in URL', () => {
|
||||||
|
const curl = 'curl https://reqbin.com/echo\\?param1\\=value1\\¶m2\\=value2';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.sendBody).toBe(false);
|
||||||
|
expect(parameters.contentType).toBeUndefined();
|
||||||
|
expect(parameters.sendHeaders).toBe(false);
|
||||||
|
expect(parameters.sendQuery).toBe(true);
|
||||||
|
expect(parameters.queryParameters).toEqual({
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'param1',
|
||||||
|
value: 'value1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'param2',
|
||||||
|
value: 'value2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with both query parameters and -d flag', () => {
|
||||||
|
const curl =
|
||||||
|
'curl -G https://reqbin.com/echo\\?param1\\=value1 -d "param2=value2" -d "param3=value3"';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.sendBody).toBe(false);
|
||||||
|
expect(parameters.contentType).toBeUndefined();
|
||||||
|
expect(parameters.sendHeaders).toBe(false);
|
||||||
|
expect(parameters.sendQuery).toBe(true);
|
||||||
|
expect(parameters.queryParameters?.parameters[0].name).toBe('param1');
|
||||||
|
expect(parameters.queryParameters?.parameters[0].value).toBe('value1');
|
||||||
|
expect(parameters.queryParameters?.parameters[1].name).toBe('param2');
|
||||||
|
expect(parameters.queryParameters?.parameters[1].value).toBe('value2');
|
||||||
|
expect(parameters.queryParameters?.parameters[2].name).toBe('param3');
|
||||||
|
expect(parameters.queryParameters?.parameters[2].value).toBe('value3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with data, defaulting to form urlencoded content type', () => {
|
||||||
|
const curl = 'curl -X POST https://reqbin.com/echo -d "key=value"';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('POST');
|
||||||
|
expect(parameters.sendBody).toBe(true);
|
||||||
|
expect(parameters.contentType).toEqual('form-urlencoded');
|
||||||
|
expect(parameters.bodyParameters?.parameters[0].name).toBe('key');
|
||||||
|
expect(parameters.bodyParameters?.parameters[0].value).toBe('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with file upload and no content type', () => {
|
||||||
|
const curl = 'curl -X POST https://reqbin.com/echo -F "file=@/path/to/file"';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('POST');
|
||||||
|
expect(parameters.sendBody).toBe(true);
|
||||||
|
expect(parameters.contentType).toBe('multipart-form-data');
|
||||||
|
expect(parameters.bodyParameters?.parameters[0].parameterType).toBe('formBinaryData');
|
||||||
|
expect(parameters.bodyParameters?.parameters[0].name).toBe('file');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with empty data flag', () => {
|
||||||
|
const curl = 'curl -X POST https://reqbin.com/echo -d ""';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('POST');
|
||||||
|
expect(parameters.sendBody).toBe(true);
|
||||||
|
expect(parameters.contentType).toEqual('form-urlencoded');
|
||||||
|
expect(parameters.bodyParameters?.parameters).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with custom header and case-insensitive content-type', () => {
|
||||||
|
const curl =
|
||||||
|
'curl -X POST https://reqbin.com/echo -H "content-TYPE: application/json" -d \'{"key":"value"}\'';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('POST');
|
||||||
|
expect(parameters.sendBody).toBe(true);
|
||||||
|
expect(parameters.contentType).toBe('json');
|
||||||
|
expect(parameters.bodyParameters?.parameters[0].name).toBe('key');
|
||||||
|
expect(parameters.bodyParameters?.parameters[0].value).toBe('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with multiple query parameters in URL', () => {
|
||||||
|
const curl =
|
||||||
|
'curl https://reqbin.com/echo\\?param1\\=value1\\¶m2\\=value2\\¶m3\\=value3';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.sendBody).toBe(false);
|
||||||
|
expect(parameters.contentType).toBeUndefined();
|
||||||
|
expect(parameters.sendHeaders).toBe(false);
|
||||||
|
expect(parameters.sendQuery).toBe(true);
|
||||||
|
expect(parameters.queryParameters?.parameters).toEqual([
|
||||||
|
{ name: 'param1', value: 'value1' },
|
||||||
|
{ name: 'param2', value: 'value2' },
|
||||||
|
{ name: 'param3', value: 'value3' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with custom header and no content type', () => {
|
||||||
|
const curl = 'curl -X GET https://reqbin.com/echo -H "Authorization: Bearer token"';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('GET');
|
||||||
|
expect(parameters.sendBody).toBe(false);
|
||||||
|
expect(parameters.contentType).toBeUndefined();
|
||||||
|
expect(parameters.sendHeaders).toBe(true);
|
||||||
|
expect(parameters.headerParameters?.parameters[0].name).toBe('Authorization');
|
||||||
|
expect(parameters.headerParameters?.parameters[0].value).toBe('Bearer token');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with empty query parameters', () => {
|
||||||
|
const curl = 'curl https://reqbin.com/echo\\?param1\\=\\¶m2=';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.sendBody).toBe(false);
|
||||||
|
expect(parameters.contentType).toBeUndefined();
|
||||||
|
expect(parameters.sendHeaders).toBe(false);
|
||||||
|
expect(parameters.sendQuery).toBe(true);
|
||||||
|
expect(parameters.queryParameters?.parameters).toEqual([
|
||||||
|
{ name: 'param1', value: '' },
|
||||||
|
{ name: 'param2', value: '' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with custom HTTP method and no body', () => {
|
||||||
|
const curl = 'curl -X PUT https://reqbin.com/echo';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('PUT');
|
||||||
|
expect(parameters.sendBody).toBe(false);
|
||||||
|
expect(parameters.contentType).toBeUndefined();
|
||||||
|
expect(parameters.sendHeaders).toBe(false);
|
||||||
|
expect(parameters.sendQuery).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with custom header and multiple data fields', () => {
|
||||||
|
const curl =
|
||||||
|
'curl -X POST https://reqbin.com/echo -H "Authorization: Bearer token" -d "key1=value1" -d "key2=value2"';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('POST');
|
||||||
|
expect(parameters.sendBody).toBe(true);
|
||||||
|
expect(parameters.contentType).toEqual('form-urlencoded');
|
||||||
|
expect(parameters.bodyParameters?.parameters).toEqual([
|
||||||
|
{ name: 'key1', value: 'value1' },
|
||||||
|
{ name: 'key2', value: 'value2' },
|
||||||
|
]);
|
||||||
|
expect(parameters.sendHeaders).toBe(true);
|
||||||
|
expect(parameters.headerParameters?.parameters[0].name).toBe('Authorization');
|
||||||
|
expect(parameters.headerParameters?.parameters[0].value).toBe('Bearer token');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with custom header and binary data', () => {
|
||||||
|
const curl =
|
||||||
|
'curl -X POST https://reqbin.com/echo -H "Content-Type: application/octet-stream" --data-binary "@/path/to/file"';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('POST');
|
||||||
|
expect(parameters.sendBody).toBe(true);
|
||||||
|
expect(parameters.contentType).toBe('raw');
|
||||||
|
expect(parameters.rawContentType).toBe('application/octet-stream');
|
||||||
|
expect(parameters.sendHeaders).toBe(false);
|
||||||
|
expect(parameters.sendQuery).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with multiple headers and case-insensitive keys', () => {
|
||||||
|
const curl =
|
||||||
|
'curl -X POST https://reqbin.com/echo -H "content-type: application/json" -H "ACCEPT: application/json" -d \'{"key":"value"}\'';
|
||||||
|
const parameters = toHttpNodeParameters(curl);
|
||||||
|
expect(parameters.url).toBe('https://reqbin.com/echo');
|
||||||
|
expect(parameters.method).toBe('POST');
|
||||||
|
expect(parameters.sendBody).toBe(true);
|
||||||
|
expect(parameters.contentType).toBe('json');
|
||||||
|
expect(parameters.bodyParameters?.parameters[0].name).toBe('key');
|
||||||
|
expect(parameters.bodyParameters?.parameters[0].value).toBe('value');
|
||||||
|
expect(parameters.sendHeaders).toBe(true);
|
||||||
|
expect(parameters.headerParameters?.parameters[0].name).toBe('ACCEPT');
|
||||||
|
expect(parameters.headerParameters?.parameters[0].value).toBe('application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should parse cURL command with no URL', () => {
|
||||||
|
const curl = 'curl -X POST -d "key=value"';
|
||||||
|
expect(() => toHttpNodeParameters(curl)).toThrow('no URL specified!');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -70,14 +70,18 @@ type ContentTypes = (typeof SUPPORTED_CONTENT_TYPES)[number];
|
|||||||
|
|
||||||
const CONTENT_TYPE_KEY = 'content-type';
|
const CONTENT_TYPE_KEY = 'content-type';
|
||||||
|
|
||||||
|
const getContentTypeHeader = (headers: JSONOutput['headers']): string | undefined => {
|
||||||
|
return get(headers, CONTENT_TYPE_KEY) ?? undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const isContentType = (headers: JSONOutput['headers'], contentType: ContentTypes): boolean => {
|
const isContentType = (headers: JSONOutput['headers'], contentType: ContentTypes): boolean => {
|
||||||
return get(headers, CONTENT_TYPE_KEY) === contentType;
|
return getContentTypeHeader(headers) === contentType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isJsonRequest = (curlJson: JSONOutput): boolean => {
|
const isJsonRequest = (curlJson: JSONOutput): boolean => {
|
||||||
if (isContentType(curlJson.headers, 'application/json')) return true;
|
if (isContentType(curlJson.headers, 'application/json')) return true;
|
||||||
|
|
||||||
if (curlJson.data) {
|
if (curlJson.data && !getContentTypeHeader(curlJson.headers)) {
|
||||||
const bodyKey = Object.keys(curlJson.data)[0];
|
const bodyKey = Object.keys(curlJson.data)[0];
|
||||||
try {
|
try {
|
||||||
JSON.parse(bodyKey);
|
JSON.parse(bodyKey);
|
||||||
@@ -91,7 +95,7 @@ const isJsonRequest = (curlJson: JSONOutput): boolean => {
|
|||||||
|
|
||||||
const isFormUrlEncodedRequest = (curlJson: JSONOutput): boolean => {
|
const isFormUrlEncodedRequest = (curlJson: JSONOutput): boolean => {
|
||||||
if (isContentType(curlJson.headers, 'application/x-www-form-urlencoded')) return true;
|
if (isContentType(curlJson.headers, 'application/x-www-form-urlencoded')) return true;
|
||||||
if (curlJson.data && !curlJson.files) return true;
|
if (!getContentTypeHeader(curlJson.headers) && curlJson.data && !curlJson.files) return true;
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,10 +152,23 @@ const extractQueries = (queries: JSONOutput['queries'] = {}): HttpNodeQueries =>
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const jsonBodyToNodeParameters = (body: JSONOutput['data'] = {}): Parameter[] | [] => {
|
const keyValueBodyToNodeParameters = (body: JSONOutput['data'] = {}): Parameter[] | [] => {
|
||||||
return Object.entries(body).map(toKeyValueArray);
|
return Object.entries(body).map(toKeyValueArray);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const jsonBodyToNodeParameters = (body: JSONOutput['data'] = {}): Parameter[] | [] => {
|
||||||
|
// curlconverter returns string if parameter includes special base64 characters like % or / or =
|
||||||
|
if (typeof body === 'string') {
|
||||||
|
// handles decoding percent-encoded characters like %3D to =
|
||||||
|
const parameters = new URLSearchParams(body);
|
||||||
|
|
||||||
|
return [...parameters.entries()].map((parameter) => {
|
||||||
|
return toKeyValueArray(parameter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return keyValueBodyToNodeParameters(body);
|
||||||
|
};
|
||||||
|
|
||||||
const multipartToNodeParameters = (
|
const multipartToNodeParameters = (
|
||||||
body: JSONOutput['data'] = {},
|
body: JSONOutput['data'] = {},
|
||||||
files: JSONOutput['files'] = {},
|
files: JSONOutput['files'] = {},
|
||||||
@@ -166,10 +183,6 @@ const multipartToNodeParameters = (
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const keyValueBodyToNodeParameters = (body: JSONOutput['data'] = {}): Parameter[] | [] => {
|
|
||||||
return Object.entries(body).map(toKeyValueArray);
|
|
||||||
};
|
|
||||||
|
|
||||||
const lowerCaseContentTypeKey = (obj: JSONOutput['headers']): void => {
|
const lowerCaseContentTypeKey = (obj: JSONOutput['headers']): void => {
|
||||||
if (!obj) return;
|
if (!obj) return;
|
||||||
|
|
||||||
@@ -220,7 +233,6 @@ export const flattenObject = <T extends Record<string, unknown>>(obj: T, prefix
|
|||||||
|
|
||||||
export const toHttpNodeParameters = (curlCommand: string): HttpNodeParameters => {
|
export const toHttpNodeParameters = (curlCommand: string): HttpNodeParameters => {
|
||||||
const curlJson = curlToJson(curlCommand);
|
const curlJson = curlToJson(curlCommand);
|
||||||
|
|
||||||
const headers = curlJson.headers ?? {};
|
const headers = curlJson.headers ?? {};
|
||||||
|
|
||||||
lowerCaseContentTypeKey(headers);
|
lowerCaseContentTypeKey(headers);
|
||||||
@@ -327,7 +339,7 @@ export const toHttpNodeParameters = (curlCommand: string): HttpNodeParameters =>
|
|||||||
sendBody: true,
|
sendBody: true,
|
||||||
specifyBody: 'keypair',
|
specifyBody: 'keypair',
|
||||||
bodyParameters: {
|
bodyParameters: {
|
||||||
parameters: keyValueBodyToNodeParameters(curlJson.data),
|
parameters: jsonBodyToNodeParameters(curlJson.data),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (isMultipartRequest(curlJson)) {
|
} else if (isMultipartRequest(curlJson)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user