Migrate to Binary-Safe Multipart Handling
Hookdeck has changed how it handles multipart/form-data requests, such as form submissions and file uploads. Multipart bodies are now handled as binary, so the original bytes reach your Destination unchanged, and Transformations and Filters receive the form fields as a structured object they can act on.
To avoid changing the behavior of existing workflows, Connections created before October 1, 2026, 18:40 UTC keep the previous behavior. This guide explains what changed, how to tell which behavior a Connection uses, and how to move to the new one.
For supported features, including structured previews, image previews, file downloads, and field-level search, see the multipart requests guide.
What Changed and Why
Hookdeck previously read multipart bodies as UTF-8 text. That works for text fields, but files and other binary content aren't valid UTF-8: while decoding them, the bytes that couldn't be read were replaced. In other words, Hookdeck was inadvertently editing the body of those requests, and the files your Destination received were not the files that were sent.
Multipart requests are now treated as binary:
- Your Destination receives the exact bytes that were sent, including files, unless a Transformation changes the multipart body. Retained files keep their original bytes.
- Transformations receive the fields as an object instead of one long string, so you no longer need to split the body on its boundary yourself.
- Filters match on fields, for example
body.event, instead of on the raw text.
Which Connections Are Affected
Each Connection uses one of two behaviors for multipart requests, called its multipart mode:
| Legacy (string) | Binary-safe | |
|---|---|---|
| Connections | Created before October 1, 2026, 18:40 UTC | Created on or after October 1, 2026, 18:40 UTC |
| Destination receives | The body decoded as UTF-8 text. File bytes can be altered. | The exact original bytes |
request.body in Transformations | The whole multipart body as a string | An object of fields |
| Filters match against | The whole multipart body as a string | The object of fields |
The mode follows the Connection's creation date relative to the rollout cutoff. Legacy Connections are pinned to legacy handling on their first multipart request. Existing Connections are not switched automatically, because a Transformation or Filter written for the legacy string would stop working. To use binary-safe handling, you create a new Connection, as described below.
Requests with other content types, such as JSON or application/x-www-form-urlencoded, are not affected.
Checking a Connection's Mode
Once a Connection using the legacy mode has received at least one multipart request, a notice appears under its rules on the Connection's Settings page.
Multipart Requests in Transformations
On a binary-safe Connection, request.body is an object with one entry per form field:
{
"email": "jane@example.com",
"tags": ["billing", "urgent"],
"attachment": {
"filename": "invoice.pdf",
"content_type": "application/pdf",
"size": 48213
}
}
- Text fields are strings.
- Repeated fields with the same name become an array, in the order they were sent.
- Files and other binary parts are described by
{ filename, content_type, size }. The file's contents are not available to your code (see Limitations).filenameisnullfor binary parts that aren't file uploads.
Repeated file fields also include part_index on each file descriptor. Preserve it when retaining or reordering files so Hookdeck can identify the original part.
Editing Fields
You can change, add, or remove text fields and return the request as-is. Hookdeck rebuilds the multipart body, and files you leave in place are sent with their original bytes.
addHandler('transform', (request, context) => {
request.body.email = request.body.email.toLowerCase();
request.body.source = 'hookdeck';
return request;
});
To drop a file, remove its field:
addHandler('transform', (request, context) => {
delete request.body.attachment;
return request;
});
Converting to JSON
To send a different format, change the content-type header and set the body. The new body replaces the multipart body, including any files.
addHandler('transform', (request, context) => {
request.headers['content-type'] = 'application/json';
request.body = {
email: request.body.email,
tags: [].concat(request.body.tags || []),
attachment_name: request.body.attachment ? request.body.attachment.filename : null,
};
return request;
});
Multipart Requests in Filters
On a binary-safe Connection, a Filter matches the fields directly. For example, this body filter only lets through submissions of a specific form:
{
"form_id": "contact-us"
}
Filters on headers, path, and query work the same way in both modes.
Limitations
- File contents can't be read or modified by Transformations yet. A Transformation can keep a file, remove it, or replace the whole body, but it can't change a file or create a new one. If a Transformation changes a file's description or replaces it with text, the original file is sent and a warning is logged in the Transformation's console. A file field that doesn't exist in the original request is left out, also with a warning.
- Unreadable multipart bodies are passed through unchanged. If a body can't be parsed as multipart, for example because a part is missing its field name,
request.bodyisnull. You can still change headers, path, and query, and the original bytes are delivered. - Mode changes need a new Connection. Updating an existing Connection, including an upsert by name through the API, CLI, or Terraform, keeps its mode.
Migrate a Connection
Moving to binary-safe handling means creating a new Connection alongside the existing one, then switching traffic over. Because both Connections share the same Source , each request is delivered by both of them while both are active, so follow the steps below to avoid duplicate deliveries.
- Write a new Transformation for the object body:
- Create a new Transformation instead of editing the existing one, which the legacy Connection still depends on.
- Update the logic to read fields from
request.bodyrather than parsing the multipart string. Filters on the body need the same update.
- Create the new Connection:
- Test the Transformation:
- Open the Transformation editor from the new Connection and select a recent multipart request as input. The input shows the object body your code receives, and you can run the Transformation to check its output.
- Once real requests arrive, inspect the held Events on the new Connection to confirm the payloads are what your Destination expects.
- Switch over:
- Cancel the Events held on the new Connection, since the existing Connection already delivered those requests.
- Disable the existing Connection.
- Unpause the new Connection. Requests that arrived after you disabled the existing Connection are held on the new one and delivered when you unpause it, so none are lost.
If your Destination can safely receive the same request twice, for example because it deduplicates by an ID in the payload, you can skip pausing and simply disable the existing Connection once the new one is working.
Using the API
The same steps apply when you manage Connections with the Hookdeck API, the CLI, or Terraform. Create the new Connection with a new name so a new Connection is created rather than the existing one updated. To test a Transformation against a real request, pass the request's original_event_data_id as event_data_id to the run transformation endpoint, along with the new Connection's ID as webhook_id.