Skip to content

everything/tasks: tasks/update checks MAX_ANSWER_BYTES against string length, so non-ASCII answers exceed the documented byte budget #5419

Open

Description

@soarkit
Field Value
Labels (suggested) bug, everything, tasks
Reproduced on commit 163a5e935756 (default branch at the time of analysis)
Introduced by commit 27b2d2415cdc0ad13782edb8b6c3cecd7846b78a — "feat(everything): serve both protocol eras and demonstrate …"

Summary

The everything server's tasks/update handler limits the size of client-supplied answers with
MAX_ANSWER_BYTES, but computes the size with JSON.stringify(answered).length. In JavaScript that is
the number of UTF-16 code units, not bytes. For any non-ASCII answer the guard under-counts by up to
3× and lets a payload that is larger than the documented byte budget through, after which the oversized
answers are handed to task.behavior.receive() and retained in the task.

Affected code

src/everything/server/tasks.ts, lines 411–424 at 163a5e935756:

      const answered = Object.fromEntries(
        Object.entries(inputResponses).filter(([key]) =>
          outstanding.includes(key),
        ),
      );
      const size = JSON.stringify(answered).length;   // <- UTF-16 code units, not bytes
      if (size > MAX_ANSWER_BYTES) {
        throw new ProtocolError(
          ProtocolErrorCode.InvalidParams,
          `Invalid params for tasks/update: the answers are ${size} bytes, over the ${MAX_ANSWER_BYTES}-byte limit; the task is unchanged`,
        );
      }
      if (Object.keys(answered).length > 0) {
        task.behavior.receive(answered, now);
      }

The constant is named MAX_ANSWER_BYTES and the error message promises a byte comparison
("the answers are N bytes"), so the unit mismatch is internal to the handler, not a documentation
question.

Expected behavior

tasks/update rejects the request with -32602 Invalid params because the serialized answers are
66 012 bytes, which is over the 65 536-byte budget; the task is left unchanged.

Actual behavior

Calling tasks/update with one outstanding answer whose text is 33 000 é characters, the handler accepts the payload and retains it in the task (MAX_ANSWER_BYTES = 65536).

Payload: one outstanding answer whose text is 33 000 U+00E9 (é) characters.
MAX_ANSWER_BYTES = 65536.

{"mode":"bug","outcome":{"accepted":true,"received":66012}}
TASKS_UPDATE_BYTE_BUDGET_BYPASSED
  • JSON.stringify(...).length = 33 012 → passes the guard.
  • UTF-8 Buffer.byteLength(..., "utf8") = 66 012 → 476 bytes over the limit.
  • The task retained the answer (received: 66012), so the documented limit was not enforced.

Control: the same probe against a copy of tasks.ts with line 416 changed to
Buffer.byteLength(JSON.stringify(answered), "utf8") produces

{"mode":"control","outcome":{"accepted":false,
  "error":"-32602: Invalid params for tasks/update: the answers are 66012 bytes, over the 65536-byte limit; the task is unchanged"}}
CONTROL_REJECTED_BY_BYTES

and retained is undefined — the divergence disappears with that one line.

Why it matters

  • The limit is a resource guard for retained task state (the answer is stored until the task settles).
    Non-ASCII answers — ordinary for any non-English client, or for base64/emoji content — get up to a 3×
    larger effective budget. 33 000 characters of CJK/Cyrillic/emoji-style text becomes ~99 KB against a
    stated 64 KiB ceiling.
  • The rejection message itself reports a byte count that is not a byte count, which makes the failure
    mode confusing for client authors.

Suggested fix

Use a byte-accurate measure:

const size = Buffer.byteLength(JSON.stringify(answered), "utf8");

(new TextEncoder().encode(...).length if the file must stay runtime-agnostic). Note that both forms
count the UTF-8 serialization of the JSON text, which is what the limit documents.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions