Transform Multipart File Uploads
When a Source receives a multipart/form-data request, a Transformation receives its form fields as an object. Each file is described by its filename, content_type and size, and has content, a Buffer holding the file's bytes.
You can read files, replace or rename them, add new files, and remove files. This guide converts an uploaded CSV file to a JSON event, then shows how to edit the files in a multipart request.
File content is available on connections that use binary-safe multipart handling. Connections created before October 1, 2026, 18:40 UTC use legacy handling; see the multipart migration guide to move one. For how Hookdeck parses multipart requests, see handle multipart requests.
Convert a CSV upload to JSON
A form uploads a CSV file in its file field, along with a note text field. This transformation parses the CSV with papaparse and replaces the multipart body with JSON.
Write and bundle the transformation
Transformations can only import Node.js built-in modules, so you bundle papaparse into your code with esbuild:
npm install papaparse
npm install --save-dev esbuild
Save this as csv-to-json.js:
import Papa from 'papaparse';
export default {
transform(request) {
const { file, ...fields } = request.body;
const { data, errors } = Papa.parse(file.content.toString('utf8'), {
header: true,
dynamicTyping: true,
skipEmptyLines: true,
});
if (errors.length) throw new Error(`Invalid CSV: ${errors[0].message}`);
request.body = { ...fields, filename: file.filename, rows: data };
request.headers['content-type'] = 'application/json';
return request;
},
};
npx esbuild csv-to-json.js --bundle --format=esm --platform=node --outfile=dist/csv-to-json.js
Paste the contents of dist/csv-to-json.js into a new transformation on your connection. See create a transformation.
Upload a file
curl -X POST "https://hkdk.events/src_xxxxxxxx" \
-F "note=October export" \
-F "file=@customers.csv;type=text/csv"
What your destination receives
For a CSV file with two rows, your destination receives Content-Type: application/json and this body:
{
"note": "October export",
"filename": "customers.csv",
"rows": [
{ "email": "jane@example.com", "name": "Jane Doe", "plan": "pro", "seats": 5 },
{ "email": "bob@example.com", "name": "Bob Lee", "plan": "free", "seats": 1 }
]
}
dynamicTyping turns numeric values such as seats into numbers. If the file isn't valid CSV, the transformation throws and Hookdeck opens a transformation issue.
Replace, add, and remove files
To keep the request as multipart and change its files, edit request.body and leave the content-type header unchanged. This transformation receives a gzip-compressed report and a debug log. It decompresses the report, adds a summary file, and removes the log:
const { gunzipSync } = require('node:zlib');
addHandler('transform', (request) => {
const { report } = request.body;
// Replace a file: decompress report.csv.gz into report.csv
report.content = gunzipSync(report.content);
report.filename = report.filename.replace(/\.gz$/, '');
report.content_type = 'text/csv';
// Add a file
const rows = report.content.toString('utf8').trim().split('\n').length - 1;
request.body.summary = {
filename: 'summary.json',
content_type: 'application/json',
content: JSON.stringify({ rows }),
};
// Remove a file
delete request.body.debug_log;
return request;
});
Upload the files:
curl -X POST "https://hkdk.events/src_xxxxxxxx" \
-F "note=nightly" \
-F "report=@report.csv.gz;type=application/gzip" \
-F "debug_log=@debug.log;type=text/plain"
Your destination receives a multipart request with the original boundary and three parts: the note field, report as report.csv with Content-Type: text/csv and the decompressed CSV, and summary as summary.json with {"rows":2}.
These rules apply when you edit files:
- Unchanged files keep their original bytes.
- Changing a file's
content,filenameorcontent_typereplaces the file.contentcan be aBuffer, aUint8Arrayor a string, and Hookdeck computessizefrom the new bytes. - Adding a field with an object that has
contentadds a file. Setfilenameandcontent_typewith it. - Deleting a field removes it, or remove an entry from a repeated field's array to remove one file.
Create a multipart upload from JSON
Some APIs only accept file uploads. To send JSON data as a file, set the content-type header to multipart/form-data and add an object with content:
addHandler('transform', (request) => {
const { orders } = request.body;
const csv = ['id,total', ...orders.map((order) => `${order.id},${order.total}`)].join('\n');
request.headers['content-type'] = 'multipart/form-data';
request.body = {
description: 'Daily orders',
file: { filename: 'orders.csv', content_type: 'text/csv', content: csv },
};
return request;
});
For a JSON request with two orders, your destination receives a multipart request with a description text field and a file part named orders.csv, with Content-Type: text/csv:
id,total
1,99.5
2,12
Hookdeck generates the multipart boundary.
Put file bytes in JSON
Changed or new file bytes can only be returned in a multipart body. To include a file in a JSON body, encode it, for example as base64:
addHandler('transform', (request) => {
const { file, ...fields } = request.body;
request.body = {
...fields,
file: {
filename: file.filename,
content_type: file.content_type,
data: file.content.toString('base64'),
},
};
request.headers['content-type'] = 'application/json';
return request;
});
Returning a Buffer inside a JSON body fails the transformation with File content can only be returned in a multipart/form-data body. To include it in another format, encode it, for example with content.toString('base64').
When you convert a multipart request to JSON and keep a file object unchanged, the JSON contains the file's filename, content_type and size, without its content.
Limits and pitfalls
- Execution time. Transformations must finish within 1 second, including the time spent parsing files.
- Memory. Each execution has 128 MB of memory, and parsing a file into objects uses much more memory than the file's size.
- Base64. Base64-encoded files are about a third larger than the original bytes.
- Text encoding.
file.content.toString('utf8')assumes the file is UTF-8. For other encodings, such as Windows-1252, bundle iconv-lite to decode it. - Older connections. On connections using legacy multipart handling, files are described by their metadata only and have no
content.
For the modules and libraries transformations can use, see Transformation Node.js compatibility.