SIGN IN SIGN UP

fix: JSON-encode object fields in multipart request bodies (#337)

`ApiClient` built its multipart body with a bare `FormData.append(key,
value)`, so any non-file field was coerced by `String()`. A `File`
survives that, an object does not.

`StreamClient.uploadFile`/`uploadImage` worked around it by
hand-stringifying `user` and `upload_sizes` behind `@ts-expect-error`.
The channel-scoped uploads — `chat.uploadChannelFile`,
`chat.uploadChannelImage`, `channel.uploadChannelFile`,
`channel.uploadChannelImage` — had no such wrapper and were broken
outright. Verified against the API:

```
Error: Stream error code 4: UploadChannelFile failed with error: "bad user id"
```

That's the server reading `"[object Object]"` as the user id.

Encoding now lives in `ApiClient.multipartBodyStringify`: files pass
through as `Blob`s, scalars keep their existing coercion, objects and
arrays get `JSON.stringify`, and no-value fields are dropped instead of
being sent as the string `"undefined"` (which the patched global path
was also doing). With one encoding site the `StreamClient` workarounds
are redundant and were removed; the overrides stay because the spec
still types `file` as `string`.

## Testing

New `__tests__/multipart.test.ts` asserts on the `FormData` that
actually reaches `fetch` — object/array/file/scalar/no-value fields, no
double-encoding of pre-encoded strings, plus all four upload entry
points. Runs in CI without credentials. Each new-behavior test was
confirmed to fail before the fix.

Also added a channel-upload suite to `__tests__/file-uploads.test.ts`,
which had no coverage — this is why the bug shipped. Both suites in that
file stay `describe.skip` per the existing convention there; run live,
all 4 pass with the fix and the 2 channel tests fail without it.
O
Oliver Lazoroski committed
5ed3f5b6ef627fdb1497bb6fbf669db213c31b0e
Parent: cd55e36
Committed by GitHub <noreply@github.com> on 8/21/2026, 2:59:46 PM