Declarative style nodes: how to send multipart/form-data?

Hi,

I want to create a node in the declarative style to consume a REST API which has some endpoints that receive multipart/form-data requests.

I didn’t find in the documentation a way to do this but following the answer here and Using the Whatsapp node as reference I’m very close to find a solution but the requests are failing.

Redirecting the request to netcat instead of the original target I can see that the body has some random characters:

POST / HTTP/1.1
Accept: application/json
Content-Type: multipart/form-data
Authorization: Bearer TOKEN
User-Agent: n8n
Host: localhost:8088
Connection: close
Transfer-Encoding: chunked

6b
----------------------------061842126287809763963984
Content-Disposition: form-data; name="first_name"


4
John
2


6a
----------------------------061842126287809763963984
Content-Disposition: form-data; name="last_name"


3
Doe
2


66
----------------------------061842126287809763963984
Content-Disposition: form-data; name="email"


c
[email protected]
3a

----------------------------061842126287809763963984--

0

If I send the request through Insomnia the body doesn’t have these characters:

POST / HTTP/1.1
Host: localhost:8088
User-Agent: insomnia/2023.2.2
Content-Type: multipart/form-data; boundary=X-INSOMNIA-BOUNDARY
Authorization: Bearer TOKEN
Accept: */*
Content-Length: 272

--X-INSOMNIA-BOUNDARY
Content-Disposition: form-data; name="first_name"

John
--X-INSOMNIA-BOUNDARY
Content-Disposition: form-data; name="last_name"

Doe
--X-INSOMNIA-BOUNDARY
Content-Disposition: form-data; name="email"

[email protected]

What I’m doing in my node is the following:

  • Define an operation with a preSend attribute:
		{
			name: 'Test',
			value: 'testOperation',
			action: 'Test',
			description: 'Tests the endpoint',
			routing: {
				send: {
					preSend: [setupRequest],
				},
				request: {
					method: 'POST',
					url: '',
                    baseURL: 'http://localhost:8088',
					headers: {
                        'Content-Type': 'multipart/form-data'
					},
				},
			},
		},
  • The setupRequest function is like this:
export async function getUploadFormData(
	this: IExecuteSingleFunctions,
): Promise<{ formData: FormData }> {
	const lastName = ((this.getNodeParameter('lastName') as string) || '').trim();
	const firstName = ((this.getNodeParameter('firstName') as string) || '').trim();
	const email = ((this.getNodeParameter('email') as string) || '').trim();

	const formData = new FormData();

	if (firstName) formData.append('first_name', firstName);
	if (lastName) formData.append('last_name', lastName);
	if (email) formData.append('email', email);

	return { formData };
}

export async function setupRequest(
	this: IExecuteSingleFunctions,
	requestOptions: IHttpRequestOptions,
) {
	const uploadData = await getUploadFormData.call(this);
	requestOptions.body = uploadData.formData;
	return requestOptions;
}
  • The parameters are defined like this:
{
		displayName: 'First name',
		name: 'firstName',
		type: 'string',
		default: '',
		displayOptions: {
			show: {
				resource: ['test'],
				operation: ['testOperation'],
			},
		},
		routing: {
			request: {
				body: {
					first_name: '={{$value)}}',
				},
			},
		},
	},
	{
		displayName: 'Last name',
		name: 'lastName',
		type: 'string',
		default: '',
		displayOptions: {
			show: {
				resource: ['test'],
				operation: ['testOperation'],
			},
		},
		routing: {
			request: {
				body: {
					last_name: '={{$value)}}',
				},
			},
		},
	},
	{
		displayName: 'Email',
		name: 'email',
		type: 'string',
		placeholder: '[email protected]',
		default: '',
		displayOptions: {
			show: {
				resource: ['test'],
				operation: ['testOperation'],
			},
		},
		routing: {
			request: {
				body: {
					email: '={{$value)}}',
				},
			},
		},
	}

Any idea of what I’m doing wrong? A missing option/encoding?

Information on your n8n setup

  • n8n version: 0.227.1
  • Database (default: SQLite): default
  • n8n EXECUTIONS_PROCESS setting (default: own, main): default
  • Running n8n via (Docker, npm, n8n cloud, desktop app): npm
  • Operating system: Mac OS

Hey @fgsalomon,

Welcome to the community :tada:

I can’t see anything in the example that would be causing there to be extra characters, Have you tried a little console logging to see what is set before the send?

1 Like

I did log the form data and it seemed to be okay:

body: FormData {
    _overheadLength: 321,
    _valueLength: 19,
    _valuesToMeasure: [],
    writable: false,
    readable: true,
    dataSize: 0,
    maxDataSize: 2097152,
    pauseStreams: true,
    _released: false,
    _streams: [
      '----------------------------231510707270787757418432\r\n' +
        'Content-Disposition: form-data; name="first_name"\r\n' +
        '\r\n',
      'John',
      [Function: bound ],
      '----------------------------231510707270787757418432\r\n' +
        'Content-Disposition: form-data; name="last_name"\r\n' +
        '\r\n',
      'Doe',
      [Function: bound ],
      '----------------------------231510707270787757418432\r\n' +
        'Content-Disposition: form-data; name="email"\r\n' +
        '\r\n',
      '[email protected]',
      [Function: bound ]
    ],
    _currentStream: null,
    _insideLoop: false,
    _pendingNext: false,
    _boundary: '--------------------------231510707270787757418432'
  },

In the end I was able to fix my issues but I don’t fully understand what’s happening…

For context: in my operation I was setting the Content-Type header to multipart/form-data because in my node requestDefaults I’ve set the Content-Type header to application/json (for the other endpoints).
As the header was missing the boundary attribute the request was not correct and was being rejected by the service.
Then reading this I assumed that if I removed the Content-Type header it was going to be set automatically with the correct boundary value (like in the Whatsapp node) but it didn’t work.

In order to make it work I’m setting the header now in setupRequest:

export async function setupRequest(
	this: IExecuteSingleFunctions,
	requestOptions: IHttpRequestOptions,
) {
	const uploadData = await getUploadFormData.call(this);
	requestOptions.body = uploadData.formData;
    if (requestOptions.headers)
		requestOptions.headers['Content-Type'] = uploadData.formData.getHeaders()['content-type'] ;
	return requestOptions;
}

Not very clean, but it works :person_shrugging:

3 Likes