Skip to content

[Bug]: truncateCloseReason can exceed its byte cap and emit U+FFFD mojibake when cutting mid-UTF-8 sequence #99976

Description

@devin-ai-integration

What happened

truncateCloseReason truncates WebSocket close reasons at the raw byte level (Buffer.subarray), which can cut a multi-byte UTF-8 sequence in half. Node then decodes the dangling partial sequence as U+FFFD replacement characters, so the returned string:

  1. contains mojibake () at the end of the close reason shown to clients, and
  2. can re-encode to more bytes than the maxBytes cap the function is supposed to enforce, because each U+FFFD is 3 bytes.

Code:

export function truncateCloseReason(reason: string, maxBytes = CLOSE_REASON_MAX_BYTES): string {
if (!reason) {
return "invalid handshake";
}
const buf = Buffer.from(reason);
if (buf.length <= maxBytes) {
return reason;
}
return buf.subarray(0, maxBytes).toString();
}

Repro

const { Buffer } = require("node:buffer");
function truncateCloseReason(reason, maxBytes = 120) {
  if (!reason) return "invalid handshake";
  const buf = Buffer.from(reason);
  if (buf.length <= maxBytes) return reason;
  return buf.subarray(0, maxBytes).toString();
}
const out = truncateCloseReason("x".repeat(118) + "😀".repeat(5));
console.log(Buffer.byteLength(out)); // 121  (> the 120-byte cap)
console.log(JSON.stringify(out.slice(-3))); // "xx�"

Expected

Output stays within maxBytes when re-encoded and never ends in a replacement character; the partial code point should be dropped instead (e.g. back up over UTF-8 continuation bytes before slicing, or decode with TextDecoder in streaming mode).

Impact

  • The function's documented RFC-safe byte cap is not actually guaranteed (CLOSE_REASON_MAX_BYTES = 120 keeps default calls under the RFC 6455 123-byte limit only by luck of the 3-byte margin; a caller passing maxBytes of 122–123 can produce a reason ws rejects with RangeError).
  • Clients receive garbled close reasons for non-ASCII handshake failure text.

Environment

Current main (4287d26).

Found via source review (AI-assisted).

Metadata

Metadata

Assignees

Labels

P2Normal backlog priority with limited blast radius.clawsweeper:current-main-reproClawSweeper found a high-confidence current-main issue reproduction.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.impact:ux-frictionUser-facing flow adds avoidable confusion or support burden without fully blocking progress.issue-rating: 🦀 challenger crabExceptional issue quality: high-confidence current-main reproduction and actionable evidence.maturity:stableIssue affects a taxonomy feature currently scored M4/M5.no-staleExclude from stale automation

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions