# 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](/docs/destinations) unchanged, and [Transformations](/docs/transformations) and [Filters](/docs/filters) receive the form fields as a structured object they can act on.

To avoid changing the behavior of existing workflows, [Connections](/docs/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](/docs/guides/multipart-requests).

## 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](#migrate-a-connection).

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:

```json
{
  "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](#limitations)). `filename` is `null` for 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.

```js
addHandler('transform', (request, context) => {
  request.body.email = request.body.email.toLowerCase();
  request.body.source = 'hookdeck';

  return request;
});

```

To drop a file, remove its field:

```js
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.

```js
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:

```json
{
  "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.body` is `null`. 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](/docs/sources), each request is delivered by both of them while both are active, so follow the steps below to avoid duplicate deliveries.

1. 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.body` rather than parsing the multipart string. Filters on the body need the same update.
2. Create the new Connection:
  
  * Use the same Source and Destination and the same rules as the existing Connection, with the new Transformation. A new Connection uses binary-safe handling.
  * [Pause](/docs/guides/how-to-pause-connections) the new Connection right away so it holds its [Events](/docs/events) instead of delivering them.
3. 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.
4. 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](/docs/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](/docs/api#transformations), along with the new Connection's ID as `webhook_id`.

### To get started with transformations, see the [transformations documentation](/docs/transformations).