Skip to content

Fix: binary request body corruption in TS→WS→Go transport#417

Open
Danny-Dasilva wants to merge 1 commit intomainfrom
fix/binary-body-corruption
Open

Fix: binary request body corruption in TS→WS→Go transport#417
Danny-Dasilva wants to merge 1 commit intomainfrom
fix/binary-body-corruption

Conversation

@Danny-Dasilva
Copy link
Copy Markdown
Owner

Summary

When a binary body is passed via options.body, the TS client calls JSON.stringify({requestId, options}). JSON cannot carry raw bytes: JSON.stringify emits bytes > 0x7F as \uXXXX escapes, Go's decoder stores them as runes, and when Go writes them back to an HTTP request body they re-emit as UTF-8 (e.g. 0xDE0xC3 0x9E). The result: upstream servers receive a corrupted payload.

The Go server already has Options.BodyBytes []byte wired as the priority branch (cycletls/index.go:195,347,1418), and encoding/json unmarshals a base64 string directly into []byte. The TS client simply wasn't using it.

This PR:

  • Expands the client-side body type to accept Buffer | Uint8Array | ArrayBuffer
  • Adds an explicit bodyBytes option
  • Auto-routes binary payloads through base64-encoded bodyBytes
  • Leaves plain string / URLSearchParams / FormData paths untouched

Reproduction (from the bug report)

const resp = await cycleTLS("https://httpbin.io/post", {
  body: Buffer.from("deadbeefabcdef123456", "hex"),
}, "post");
console.log((await resp.json()).data);

Before: data:application/octet-stream;base64,w57CrcK-w6_Cq8ONw68SNFY= (UTF-8 mangled)
After: data:application/octet-stream;base64,3q2-76vN7xI0Vg== (matches native fetch)

Test plan

  • New tests/binary-body-roundtrip.test.ts with 5 cases (local HTTP echo server, byte-for-byte verification):
    • Buffer body with high bytes
    • All 256 byte values round-trip (SHA-256 equality)
    • Uint8Array body auto-routed to bodyBytes
    • Explicit bodyBytes option
    • Plain-string body still flows through body
  • Existing tests/urlencoded.test.ts and tests/multipartFormData.test.ts pass unchanged
  • Manual verification against https://httpbin.io/post reproduces the original bug and confirms the fix

Notes

  • No wire-format change on the Go side — BodyBytes was already there.
  • No breaking change: existing string-body callers continue to work.
  • If you prefer the "always base64 every body" approach (as suggested in the issue comment), that would be a breaking wire change and can follow as a v3 cleanup.

🤖 Generated with Claude Code

When a binary body (Buffer/Uint8Array/ArrayBuffer) is passed to the
TS client, route it through the existing `bodyBytes` field as base64.
JSON cannot carry raw bytes: `JSON.stringify` emits bytes > 0x7F as
\uXXXX escapes, Go's decoder stores them as runes, and they re-emit
as UTF-8 (e.g. 0xDE → 0xC3 0x9E) — corrupting the request body.

Go already has `Options.BodyBytes []byte` wired with priority over
`Body` (cycletls/index.go:195,347,1418); `encoding/json` unmarshals
base64 strings directly into []byte. The client just wasn't using it.

- Expand `body` type to accept Buffer | Uint8Array | ArrayBuffer
- Add explicit `bodyBytes` option for callers that want to be explicit
- Auto-route binary `body` to `bodyBytes` in `sendRequest`
- Plain string bodies remain unchanged (URLSearchParams / FormData /
  string paths untouched)
- Add regression test covering Buffer, Uint8Array, explicit bodyBytes,
  all-256-byte-values round-trip, and string pass-through

Verified against the user's httpbin.io repro — CycleTLS now returns
the same base64 payload as `fetch`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant