Custom Node: multipart/form-data upload fails with "missing parameter" (works in HTTP Request node)

Describe the problem/error/question
I am developing a custom node for the Eulerian Technologies API. I need to upload a CSV file.

I am facing a strange issue where my custom node fails to upload the file, whereas the standard n8n HTTP Request node — configured with the exact same parameters — works perfectly (with binaries from CSV data).

The API returns an error stating it cannot find the file parameter, which suggests that the multipart/form-data body (or the boundary) is not being constructed correctly by this.helpers.request in my custom node, even though the configuration matches the working HTTP node.

What is the error message?
The API responds with a 200 OK (so auth and URL are correct) but returns this JSON error: {"error_msg": "externaldataupload_advertising | missing file-name parameter:", "error": true}

This implies the API cannot parse the specific form field named file-name.

HTTP Request node configuration (this works) :

  • Method: Post
  • URL: https://{customer}.api.eulerian.{datacenter}/ea/v2/ea/{site}/db/ope/externaldataupload_advertising.json
  • Authentification: None
  • Send Headers: true
    • Specify Headers: Using Fields Below
      • Name: Authorization
      • Value : Bearer ${mytoken}
  • Send Body: true
    • Body Content Type: Form-Data
      • Parameter Type: n8n Binary File
      • Name: file-name
      • Input Data Field Name: data

Request Options generated by HTTP Request node:

{“headers”:{“Authorization”:“Bearer ${mytoken}”,“accept”:“application/json,text/html,application/xhtml+xml,application/xml,text/;q=0.9, image/;q=0.8, /;q=0.7”},“method”:“POST”,“uri”:“https://{customer}.api.eulerian.{datacenter}/ea/v2/ea/{site}/db/ope/externaldataupload_advertising.json",“gzip”:true,“rejectUnauthorized”:true,“followRedirect”:false,“resolveWithFullResponse”:true,“timeout”:300000,“formData”:{“file-name”:{“value”:{“type”:“Buffer”,“data”:[101,97,58,100,97,116,101,59,101,97,58,111,112,101,59,101,97,58,108,111,99,97,116,105,111,110,59,101,97,58,99,114,101,97,116,105,118,101,59,101,97,58,101,120,116,101,114,110,97,108,95,118,105,101,119,59,101,97,58,101,120,116,101,114,110,97,108,95,99,108,105,99,107,59,101,97,58,101,120,116,101,114,110,97,108,95,99,111,115,116,10,50,48,49,52,45,48,49,45,48,49,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,49,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,49,50,51,52,53,59,48,59,48,10,50,48,49,52,45,48,49,45,48,49,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,49,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,49,50,51,59,50,59,48,10,50,48,49,52,45,48,49,45,48,49,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,50,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,49,54,53,59,50,49,59,48,46,50,51,10,50,48,49,52,45,48,49,45,48,50,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,49,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,53,59,48,59,48,46,49,50,10,50,48,49,52,45,48,49,45,48,51,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,49,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,53,48,48,59,49,50,59,49,53,46,50,10,50,48,49,52,45,48,49,45,48,52,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,49,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,48,59,50,59,49,46,50,10]},“options”:{“filename”:“data-to-upload-advertising.csv”,“contentType”:“text/csv”}}},“encoding”:null,“json”:false,"useStream”:true}

Request Response from HTTP Request node:

[{“error”: false,“data”: {“fields”: ,“rows”: },“meta”: {“reqid”: “14EDEA713F1E5E43CB308E2A4A1FAB8F”,“tm”: 1765492769,“host”: “er12”,“pid”: 22168,“total”: 0,“elapsed”: 345.192,“start”: 0}}]

The code from my custom node:

let requestConf: any = {
          method: httpMethod,
          url,
          headers: {
            'Content-Type': 'application/json'
          }
        };

        if (!requestConf.headers) {
          requestConf.headers = {};
        }

        if(edwSessionToken){
          requestConf.headers['Authorization'] = `Bearer ${edwSessionToken}`;
        }
        else {          
          requestConf.headers['Authorization'] = `Bearer ${apiAuthentificationToken}`;
        }

        if (uploadOperations.includes(operation)) {

          const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
          const itemBinaryData = this.helpers.assertBinaryData(i, binaryPropertyName);

          const uploadBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);

          requestConf.formData = {
            'file-name': {
              value: uploadBuffer,
              options: {
                filename: itemBinaryData.fileName,
                contentType: itemBinaryData.mimeType,
              }
            }
          };

          delete requestConf.headers['Content-Type']; 
          requestConf.headers['accept'] = 'application/json,text/html,application/xhtml+xml,application/xml,text/*;q=0.9, image/*;q=0.8, */*;q=0.7';
          requestConf['gzip'] = true;
          requestConf['rejectUnauthorized'] = true;
          requestConf['followRedirect'] = false;
          requestConf['resolveWithFullResponse'] = true;
          requestConf['encoding'] = null;
          requestConf['json'] = false;
          requestConf['useStream'] = true;
          requestConf['timeout'] = 300000;
        
        } else {
          if (!operation.includes('Delete') && ['DELETE', 'PATCH', 'POST', 'PUT'].includes(httpMethod)) {
            requestConf.body = JSON.parse(requestBody);
          }
        }
        
        console.log('url : ' + url);
        console.log('requestConf : ' + JSON.stringify(requestConf));

        const responseData = await this.helpers.httpRequest(requestConf);

        console.log('responseData : ' + responseData);

				if (typeof responseData === 'string') {
          const trimmed = responseData.trim();
          if (trimmed !== '') {
            try {
              returnData.push({ json: JSON.parse(trimmed) });
            } catch {
              returnData.push({ text: trimmed });
            }
          } else {
            returnData.push({ 'Status Code': '204 No Content' });
          }
        } else if (responseData) {
          returnData.push(responseData);
        } else {
          returnData.push({ 'Status Code': '204 No Content' });
        }      

Request Options generated by my custom node:

{"method":"POST","url":"https://{customer}.api.eulerian.{datacenter}/ea/v2/ea/{site}/db/ope/externaldataupload_advertising.json?file-name=data-to-upload-advertising.csv","headers":{"Authorization":"Bearer ${mytoken}","accept":"application/json,text/html,application/xhtml+xml,application/xml,text/*;q=0.9, image/*;q=0.8, */*;q=0.7"},"formData":{"file-name":{"value":{"type":"Buffer","data":[101,97,58,100,97,116,101,59,101,97,58,111,112,101,59,101,97,58,108,111,99,97,116,105,111,110,59,101,97,58,99,114,101,97,116,105,118,101,59,101,97,58,101,120,116,101,114,110,97,108,95,118,105,101,119,59,101,97,58,101,120,116,101,114,110,97,108,95,99,108,105,99,107,59,101,97,58,101,120,116,101,114,110,97,108,95,99,111,115,116,10,50,48,49,52,45,48,49,45,48,49,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,49,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,49,50,51,52,53,59,48,59,48,10,50,48,49,52,45,48,49,45,48,49,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,49,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,49,50,51,59,50,59,48,10,50,48,49,52,45,48,49,45,48,49,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,50,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,49,54,53,59,50,49,59,48,46,50,51,10,50,48,49,52,45,48,49,45,48,50,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,49,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,53,59,48,59,48,46,49,50,10,50,48,49,52,45,48,49,45,48,51,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,49,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,53,48,48,59,49,50,59,49,53,46,50,10,50,48,49,52,45,48,49,45,48,52,59,67,97,109,112,97,105,103,110,95,110,97,109,101,95,49,59,76,111,99,97,116,105,111,110,95,49,59,67,114,101,97,116,105,118,101,95,49,59,48,59,50,59,49,46,50,10]},"options":{"filename":"data-to-upload-advertising.csv","contentType":"text/csv"}}},"gzip":true,"rejectUnauthorized":true,"followRedirect":false,"resolveWithFullResponse":true,"encoding":null,"json":false,"useStream":true,"timeout":300000}

Request Response from my custom node:

[
  {
    "meta": {
      "reqid": "5EBB111C7A306A695567E9782196CF02",
      "tm": 1765493089,
      "host": "er11",
      "elapsed": 22.941,
      "pid": 27008
    },
    "error_msg": "externaldataupload_advertising | missing file-name parameter: ",
    "error": true
  }
]

Thanks for reading!

Any help will be appreciate!

1 Like

hello @Guillaume_Sinnaeve

I’ve managed to upload files with the code below, according to the service doc:

export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
	let query: IDataObject = { cid: this.getNodeParameter('cid', i, 0) as number };
	let body: IDataObject | FormData = {};
	let response: INodeExecutionData[];

	let uploadData: Buffer | Readable;

	const binaryName = (this.getNodeParameter('binaryName', i, '') as string).trim();
	const binaryData = this.helpers.assertBinaryData(i, binaryName);

	if (binaryData.id) {
		uploadData = await this.helpers.getBinaryStream(binaryData.id);
	} else {
		uploadData = Buffer.from(binaryData.data, BINARY_ENCODING);
	}

	const fileName = binaryData.fileName as string;
	if (!fileName)
		throw new NodeOperationError(this.getNode(), 'No file name given for file upload.');

	const formData = {
		file_content: {
			value: uploadData,
			options: {
				filename: fileName,
				contentType: binaryData.mimeType,
			},
		},
		file_original_name: fileName,
		...body,
	};

	response = await apiRequest.call(
		this,
		'POST',
		(`${endpoint}/file/add/` + this.getNodeParameter('folderId', i, 0)) as string,
		formData,
		query,
		{},
		true,
	);

	const executionData = this.helpers.constructExecutionMetaData(
		this.helpers.returnJsonArray(response as IDataObject[]),
		{ itemData: { item: i } },
	);

	return executionData;
}

export async function apiRequest(
	this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
	method: IHttpRequestMethods,
	endpoint: string,
	body: IDataObject | FormData = {},
	query?: IDataObject,
	option: IDataObject = {},
	isFormData: boolean = false,
): Promise<any> {
	const credentials = await this.getCredentials('dfirIrisApi');
	const baseUrl = (credentials?.isHttp ? 'http://' : 'https://') + credentials?.host;
	let headers = { 'content-type': 'application/json; charset=utf-8' };

	if (isFormData) headers = { 'content-type': 'multipart/form-data; charset=utf-8' };

	query = query || {};

	const disableSslChecks = credentials.isHttp
		? true
		: (credentials.allowUnauthorizedCerts as boolean);

	let options: IHttpRequestOptions = {
		headers: headers,
		method,
		url: `${baseUrl}/${endpoint}`,
		body,
		qs: query,
		json: true,
		skipSslCertificateValidation: disableSslChecks,
		ignoreHttpStatusErrors: true,
	};
	if (Object.keys(option).length > 0) {
		options = Object.assign({}, options, option);
	}

	if (Object.keys(body).length === 0) {
		delete options.body;
	}

	if (Object.keys(query).length === 0) {
		delete options.qs;
	}

	Object.assign(options, { rejectUnauthorized: disableSslChecks });

	try {
		customDebug('options', options)
		return await this.helpers.requestWithAuthentication.call(this, 'dfirIrisApi', options);
	} catch (error) {
		throw new NodeApiError(this.getNode(), error as JsonObject);
	}
}

The best option is to open some recent nodes that have the file upload operation. And check the source code there. HTTP node may not be very convenient, as it’s kinda special

1 Like

Thanks @barn4k for your time and your answer.

I changed the structure of my node to follow your example and I still have the same issue.

I shared my code through github : n8n-nodes-euleriantechnologies/nodes/EulerianTechnologies/EulerianTechnologies.node.ts at main · guiElevate/n8n-nodes-euleriantechnologies · GitHub

The error still is

[
  {
    "meta": {
      "reqid": "5EBB111C7A306A695567E9782196CF02",
      "tm": 1765493089,
      "host": "er11",
      "elapsed": 22.941,
      "pid": 27008
    },
    "error_msg": "externaldataupload_advertising | missing file-name parameter: ",
    "error": true
  }
]

And i pushed ‘file-name’. Tried multiple method tu push it

body = {
        file: {
          value: uploadData,
          options: {
            filename: fileName,
            contentType: binaryData.mimeType
          },
        },
        'file-name': fileName
      };
body = {
        'file-name': {
          value: uploadData,
          options: {
            filename: fileName,
            contentType: binaryData.mimeType
          },
        }
      };

Maybe i missed something in the documentation?

When i tried with CURL, it works

curl -H 'Authorization: Bearer {apitoken}' -X POST  -F [email protected] https://{customer}.api.eulerian.{datacenter}/ea/v2/ea/{site}/db/ope/externaldataupload_advertising.json

it’s also work in JS with node

const axios = require("axios");
const FormData = require("form-data");
const fs = require("fs");
const path = require("path"); // Utile pour les chemins

const filePath = "data-to-upload-advertising.csv";

const binaryDataBuffer = fs.readFileSync(filePath);

const fileName = path.basename(filePath);
const mimeType = 'text/csv'; // Type MIME du fichier

async function uploadFileFromBuffer() {
    const url = "https://{customer}.api.eulerian.{datacenter}/ea/v2/ea/{site}/db/ope/externaldataupload_advertising.json";
    const token = "MY_TOKEN";
    
    const form = new FormData();
    
    form.append("file-name", binaryDataBuffer, {
        filename: fileName,
        contentType: mimeType,
    });

    const config = {
        method: 'POST',
        url: url,
        headers: {
            ...form.getHeaders(),
            Authorization: `Bearer ${token}`,
            Accept: 'application/json, text/plain, */*'
        },
        data: form
    };

    console.log('config : ' + JSON.stringify(config))

    try {
        const response = await axios(config);
        console.log("✅ Success:", response.data);
    } catch (error) {
        console.error("❌ Upload failed:", error.response?.data || error.message);
    }
}

uploadFileFromBuffer();

The API endpoint is quite strange: it should be form data, but they advise using JSON…

Have you tried setting the content type to form data?

`headers = { ‘content-type’: ‘multipart/form-data; charset=utf-8’ }`

Yes, i tried with content-type = multipart/form-data; boundary=--------------------------027073295633341578311899 but when i set it like this, i have 502 Error (Bad gateway - the service failed to handle your request).

{
  "data": {
    "_overheadLength": 175,
    "_valueLength": 444,
    "_valuesToMeasure": [],
    "writable": false,
    "readable": true,
    "dataSize": 0,
    "maxDataSize": 2097152,
    "pauseStreams": true,
    "_released": false,
    "_streams": [
      "----------------------------027073295633341578311899\r\nContent-Disposition: form-data; name=\"file-name\"; filename=\"data-to-upload-advertising.csv\"\r\nContent-Type: text/csv\r\n\r\n",
      {
        "type": "Buffer",
        "data": [
          101,
          97,
          58,
          100,
          97,
          116,
          101,
          59,
          101,
          97,
          58,
          111,
          112,
          101,
          59,
          101,
          97,
          58,
          108,
          111,
          99,
          97,
          116,
          105,
          111,
          110,
          59,
          101,
          97,
          58,
          99,
          114,
          101,
          97,
          116,
          105,
          118,
          101,
          59,
          101,
          97,
          58,
          101,
          120,
          116,
          101,
          114,
          110,
          97,
          108,
          95,
          118,
          105,
          101,
          119,
          59,
          101,
          97,
          58,
          101,
          120,
          116,
          101,
          114,
          110,
          97,
          108,
          95,
          99,
          108,
          105,
          99,
          107,
          59,
          101,
          97,
          58,
          101,
          120,
          116,
          101,
          114,
          110,
          97,
          108,
          95,
          99,
          111,
          115,
          116,
          10,
          50,
          48,
          49,
          52,
          45,
          48,
          49,
          45,
          48,
          49,
          59,
          67,
          97,
          109,
          112,
          97,
          105,
          103,
          110,
          95,
          110,
          97,
          109,
          101,
          95,
          49,
          59,
          76,
          111,
          99,
          97,
          116,
          105,
          111,
          110,
          95,
          49,
          59,
          67,
          114,
          101,
          97,
          116,
          105,
          118,
          101,
          95,
          49,
          59,
          49,
          50,
          51,
          52,
          53,
          59,
          48,
          59,
          48,
          10,
          50,
          48,
          49,
          52,
          45,
          48,
          49,
          45,
          48,
          49,
          59,
          67,
          97,
          109,
          112,
          97,
          105,
          103,
          110,
          95,
          110,
          97,
          109,
          101,
          95,
          49,
          59,
          76,
          111,
          99,
          97,
          116,
          105,
          111,
          110,
          95,
          49,
          59,
          67,
          114,
          101,
          97,
          116,
          105,
          118,
          101,
          95,
          49,
          59,
          49,
          50,
          51,
          59,
          50,
          59,
          48,
          10,
          50,
          48,
          49,
          52,
          45,
          48,
          49,
          45,
          48,
          49,
          59,
          67,
          97,
          109,
          112,
          97,
          105,
          103,
          110,
          95,
          110,
          97,
          109,
          101,
          95,
          50,
          59,
          76,
          111,
          99,
          97,
          116,
          105,
          111,
          110,
          95,
          49,
          59,
          67,
          114,
          101,
          97,
          116,
          105,
          118,
          101,
          95,
          49,
          59,
          49,
          54,
          53,
          59,
          50,
          49,
          59,
          48,
          46,
          50,
          51,
          10,
          50,
          48,
          49,
          52,
          45,
          48,
          49,
          45,
          48,
          50,
          59,
          67,
          97,
          109,
          112,
          97,
          105,
          103,
          110,
          95,
          110,
          97,
          109,
          101,
          95,
          49,
          59,
          76,
          111,
          99,
          97,
          116,
          105,
          111,
          110,
          95,
          49,
          59,
          67,
          114,
          101,
          97,
          116,
          105,
          118,
          101,
          95,
          49,
          59,
          53,
          59,
          48,
          59,
          48,
          46,
          49,
          50,
          10,
          50,
          48,
          49,
          52,
          45,
          48,
          49,
          45,
          48,
          51,
          59,
          67,
          97,
          109,
          112,
          97,
          105,
          103,
          110,
          95,
          110,
          97,
          109,
          101,
          95,
          49,
          59,
          76,
          111,
          99,
          97,
          116,
          105,
          111,
          110,
          95,
          49,
          59,
          67,
          114,
          101,
          97,
          116,
          105,
          118,
          101,
          95,
          49,
          59,
          53,
          48,
          48,
          59,
          49,
          50,
          59,
          49,
          53,
          46,
          50,
          10,
          50,
          48,
          49,
          52,
          45,
          48,
          49,
          45,
          48,
          52,
          59,
          67,
          97,
          109,
          112,
          97,
          105,
          103,
          110,
          95,
          110,
          97,
          109,
          101,
          95,
          49,
          59,
          76,
          111,
          99,
          97,
          116,
          105,
          111,
          110,
          95,
          49,
          59,
          67,
          114,
          101,
          97,
          116,
          105,
          118,
          101,
          95,
          49,
          59,
          48,
          59,
          50,
          59,
          49,
          46,
          50,
          10
        ]
      },
      null
    ],
    "_currentStream": null,
    "_insideLoop": false,
    "_pendingNext": false,
    "_boundary": "--------------------------027073295633341578311899"
  },
  "headers": {
    "Authorization": "Bearer MY_TOKEN",
    "content-type": "multipart/form-data; boundary=--------------------------027073295633341578311899"
  },
  "json": false,
  "method": "POST",
  "url": "https://{customer}.api.eulerian.{datacenter}/ea/v2/ea/{site}/db/ope/externaldataupload_advertising.json"
}

It’s weird…

Finally, i find the solution!

I was trying to “force” the work manually (import form-data). This conflicted with n8n’s HTTP proxy/wrapper, generating malformed requests (often without the correct Content-Length or with chunking problems, hence the 502).

I use the following codes

isFormData = true;
    let uploadData: Buffer | Readable;

    const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
    const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);

    if (binaryData.id) {
      uploadData = await this.helpers.getBinaryStream(binaryData.id);
    } else {
      uploadData = Buffer.from(binaryData.data, BINARY_ENCODING);
    }

    const fileName = binaryData.fileName as string;
    if (!fileName) throw new NodeOperationError(this.getNode(), 'No file name given for file upload.');

    body = {
      'file-name': {
        value: uploadData,
        options: {
          filename: fileName,
          contentType: binaryData.mimeType,
        }
      }
    };
if (isFormData) {
    (options as any).formData = body;
  } else {
    options.body = body;
  }

I will push the complete node on github and share it with the community.

Thanks a lot for your help!

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.