Skip to content

[BUG] Content-Length set from String.length (UTF-16), not UTF-8 byte length — truncates multi-byte request bodies #1155

@terraelise

Description

@terraelise

Is there an existing issue for this?

  • I have searched the existing issues

SDK Version

6.4.0 (@optimizely/optimizely-sdk); also on master as of June 2026. Node.js 18+ (tested on 24).

Current Behavior

Summary

RequestHandler.makeRequest (Node) sets the content-length header from the request body's JavaScript string .length (UTF-16 code-unit count), but writes the body to the socket as UTF-8. For any request body containing multi-byte UTF-8 characters (non-Latin text, full-width punctuation, emoji, accented characters), the header under-reports the actual byte size, so the receiving server gets a truncated body.

For the event dispatcher this shows up as 400 responses from the events endpoint (https://logx.optimizely.com/v1/events) with an "Invalid json" / parse error, because the JSON is cut off mid-string.

Affected code

lib/utils/http_request_handler/request_handler.node.ts, in makeRequest:

headers: {
  ...headers,
  'content-length': String(data?.length || 0), // ← UTF-16 code units, not bytes
},
...
if (data) {
  request.write(data); // ← written as UTF-8
}

Present in @optimizely/optimizely-sdk@6.4.0 and still on master as of June 2026.

Why it's wrong

String.prototype.length counts UTF-16 code units, not bytes; request.write(string) encodes as UTF-8 by default. The two differ for any character outside the single-byte range, so Content-Length is too small and the body is truncated.

Impact

  • Any event whose payload contains a multi-byte character is rejected (HTTP 400, truncated/invalid JSON).
  • Events are batched, so a single multi-byte event poisons the entire batch's Content-Length — co-batched ASCII-only events fail too.
  • Affects any app sending non-ASCII tag values/attributes (Japanese, emoji, accented names, smart quotes, etc.). It looks intermittent/random because it depends on the byte composition of each batch.

Expected Behavior

Content-Length should equal the UTF-8 byte length of the request body, so multi-byte payloads are sent intact and accepted by the events endpoint.

Steps To Reproduce

  • @optimizely/optimizely-sdk: 6.4.0 (also reproduces on master)
  • Node.js: 18+ (tested on 24)
  • Platform: Node server

Standalone Node script (no SDK required) that mimics the handler — sets Content-Length from String.length and writes the body as UTF-8:

const http = require('http');
const body = '{"searchTerm":"2022 ONE PIECE ACE OP02-013"}'; // full-width spaces (U+3000)

const server = http.createServer((req, res) => {
  const chunks = [];
  req.on('data', (c) => chunks.push(c));
  req.on('end', () => {
    const received = Buffer.concat(chunks).toString('utf8');
    let parse;
    try { JSON.parse(received); parse = 'OK'; }
    catch (e) { parse = 'PARSE ERROR: ' + e.message; }
    console.log(`content-length=${req.headers['content-length']} bytesRead=${Buffer.byteLength(received)} parse=${parse}`);
    res.end('ok');
  });
});

function send(contentLength, port) {
  return new Promise((resolve) => {
    const req = http.request({ port, method: 'POST',
      headers: { 'content-type': 'application/json', 'content-length': String(contentLength), connection: 'close' } },
      (res) => { res.on('data', () => {}); res.on('end', resolve); });
    req.on('error', () => resolve());
    req.write(body); // UTF-8
    req.end();
  });
}

server.listen(0, async () => {
  const { port } = server.address();
  console.log(`UTF-16 .length=${body.length} UTF-8 byteLength=${Buffer.byteLength(body)}`);
  await send(body.length, port);              // SDK behavior
  await new Promise((r) => setTimeout(r, 50));
  await send(Buffer.byteLength(body), port);  // correct
  server.close();
});

Output:

UTF-16 .length=44 UTF-8 byteLength=52
content-length=44 bytesRead=44 parse=PARSE ERROR: Unterminated string in JSON at position 36
content-length=52 bytesRead=52 parse=OK

SDK Type

Node

Node Version

Node v24

Browsers impacted

No response

Link

No response

Logs

No response

Severity

No response

Workaround/Solution

Use the UTF-8 byte length for Content-Length:

'content-length': String(data ? Buffer.byteLength(data, 'utf8') : 0),

(Browser / React Native request handlers should use the platform-appropriate byte length, e.g. new TextEncoder().encode(data).length.)

Recent Change

No response

Conflicts

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions