can1357/oh-my-pi · error

data must be a string

Error message

data must be a string

What it means

For the debug server's "bytes" operation, #dispatch requires request.data to be a string, which is injected verbatim as a raw input sequence into the TUI. If data is missing or not a string, this error is thrown and returned as a failed response. Raw byte payloads must be provided as (escaped) text in JSON.

Source

Thrown at packages/tui/src/debug-server.ts:359

				return {
					ok: true,
					columns: this.#tui.terminal.columns,
					rows: this.#tui.terminal.rows,
					pid: process.pid,
					overlays: this.#tui.overlayStack.length,
					focused: focused === null ? null : componentKind(focused),
					alt_screen: paint?.altScreen ?? false,
					cursor: paint?.cursor ?? { visible: false },
				};
			}
			case "keys": {
				if (typeof request.keys !== "string") throw new Error("keys must be a string");
				const parsed = parseKeyTokens(request.keys);
				for (const sequence of parsed.sequences) this.#tui.injectDebugInput(sequence);
				return { ok: true, injected: parsed.events };
			}
			case "bytes":
				if (typeof request.data !== "string") throw new Error("data must be a string");
				this.#tui.injectDebugInput(request.data);
				return { ok: true };
			case "paste":
				if (typeof request.text !== "string") throw new Error("text must be a string");
				this.#tui.injectDebugInput(`\x1b[200~${request.text}\x1b[201~`);
				return { ok: true };
			case "mouse": {
				if (typeof request.x !== "number" || typeof request.y !== "number")
					throw new Error("mouse x and y must be numbers");
				const action = request.action === undefined ? "click" : request.action;
				if (typeof action !== "string") throw new Error("mouse action must be a string");
				this.#tui.injectDebugInput(mouseSequence(request.x, request.y, action));
				return { ok: true };
			}
			case "quit":
				return { ok: true };
			default:
				return { ok: false, error: `unknown op ${request.op}` };

View on GitHub (pinned to 9690622007)

Solutions

  1. Set data to a JSON string containing the raw sequence with escapes, e.g. "\u001b[A" for arrow-up
  2. If you have a byte array, decode it to a UTF-8/latin1 string before sending
  3. Use the "paste" or "keys" ops instead when they match the intent

Example fix

// before
{ "op": "bytes", "data": [27, 91, 65] }
// after
{ "op": "bytes", "data": "\u001b[A" }
Defensive patterns

Strategy: validation

Validate before calling

if (typeof request.data !== "string")
  request.data = Buffer.from(request.data as number[]).toString("latin1");

Type guard

function hasStringData(r: { op: string; data?: unknown }): r is { op: "bytes"; data: string } {
  return r.op === "bytes" && typeof r.data === "string";
}

Try / catch

try {
  const res = await send({ op: "bytes", data });
} catch (err) {
  if (String(err.message) === "data must be a string") {
    data = String.fromCharCode(...(data as number[]));
  }
}

Prevention

When it happens

Trigger: Sending {"op":"bytes"} without data; sending data as an object like {"bytes":[27,91]}, a number, or a nested object.

Common situations: Clients that base64-encode or numeric-encode escape sequences and send the raw buffer instead of a JSON string; forgetting JSON-escaping for control characters like ESC.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/5f06fd7702e58c59. Report an issue: GitHub.