Skip to content

Large HTTP responses can crash the CLI with Undici Parser.finish assertion #4

Closed

Description

@ClarkvdM

Problem

t3code --json request get can terminate with an uncaught Undici assertion when reading a large T3 response. It emits a stack trace instead of the CLI's JSON error envelope. The agent in desktop T3 thread dd878153-f871-437d-8ffd-d097e3902138 fell back to opening state.sqlite read-only to retrieve the requested conversation.

Evidence from desktop-zolder

The thread's recorded tool results on 2026-09-18 show:

  • Installed @bvdm/t3code-cli version: 0.1.2 (also verified during this investigation).
  • T3 server: 0.0.43-nightly.20260918.1895.
  • Windows Node 26.2.0; its bundled Undici is 8.3.0.
  • The full /api/orchestration/snapshot request failed. Requests to /api/orchestration/threads/5e5fe859-8f7d-4f56-ab85-a2ed3ea65aef and /api/orchestration/snapshot?threadId=5e5fe859-8f7d-4f56-ab85-a2ed3ea65aef recorded this assertion:
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:

  assert(!this.paused)

    at Parser.finish (node:internal/deps/undici/undici:7247:9)
    at Socket.onHttpSocketEnd (node:internal/deps/undici/undici:7679:34)
    at Socket.emit (node:events:521:24)
    at endReadableNT (node:internal/streams/readable:1744:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:90:21)

Windows additionally reported Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 94.

Retrying the thread request through T3's Electron runtime also failed: Node 24.19.0, Undici 7.29.0, the same Parser.finish assertion, exit code 1, and a zero-byte output file. Changing to that Node 24 runtime is therefore not a workaround.

Cause

This matches the upstream HTTP/1 parser bug nodejs/undici#5360. When response backpressure pauses the parser and the peer ends the socket, onHttpSocketEnd invokes Parser.finish, which asserts that the parser is not paused. The exception escapes from the socket event handler rather than rejecting the fetch promise. Upstream merged a fix in nodejs/undici#5474.

The CLI exposes this path by using Node's bundled global fetch and explicitly sending connection: "close" in src/api.ts. Its promise .catch() cannot contain this uncaught event-handler assertion. Process termination also prevents the session-revocation finally from completing; the short session TTL remains the fallback.

The evidence points to a parser/backpressure/socket-close interaction, not a fixed maximum JSON size. Merely increasing a timeout or buffer limit will not fix it. Removing the close header is worth testing, but is not sufficient protection if the server closes the connection itself.

Reproduction

With a running T3 instance containing substantial conversation history:

t3code --json request get /api/orchestration/snapshot | Out-File -Encoding utf8 snapshot.json

The following isolated transport reproduction needs no T3 credentials or conversation data. Save it as repro.cjs and run node repro.cjs. It intentionally delays body consumption to exercise backpressure:

const { createServer } = require('node:net');
const body = Buffer.alloc(64 * 1024, 97);
const server = createServer(socket => {
  socket.once('data', () => {
    socket.write(
      'HTTP/1.1 200 OK\r\n' +
      `Content-Length: ${body.length}\r\n` +
      'Connection: close\r\n\r\n'
    );
    socket.end(body);
  });
});
server.listen(0, '127.0.0.1', async () => {
  try {
    const response = await fetch(`http://127.0.0.1:${server.address().port}`, {
      headers: { connection: 'close' },
    });
    await new Promise(resolve => setTimeout(resolve, 100));
    console.log((await response.text()).length);
  } catch (error) {
    console.error('Caught request error:', error.message);
  } finally {
    server.close();
  }
});

Investigation results:

  • Linux Node 26.3.0 / Undici 8.3.0: reproduced the identical uncaught assertion, exit 1, bypassing the catch.
  • Windows Node 26.2.0 / Undici 8.3.0: this small synthetic fixture completed in the trial. The original Windows CLI crashes are recorded in the thread. Trigger timing and socket buffering matter.
  • A normally consumed 8 MiB synthetic JSON response completed with and without Connection: close on Linux. Size alone does not reproduce the fault.
  • The desktop's current doctor reports the T3 server unavailable, so this investigation used recorded desktop failures and an isolated transport fixture rather than claiming a fresh live T3 reproduction.

Suggested fix and acceptance criteria

  • Use a transport with the upstream parser fix, or a suitable node:http/node:https implementation. Adding an Undici dependency alone will not change global fetch; the request path must actually use the fixed implementation.
  • Review the forced Connection: close behavior and manage transport shutdown explicitly.
  • Add a child-process regression test for a response that closes while body consumption is backpressured. Cover complete and truncated bodies so assertions cannot take down the test runner.
  • Preserve the JSON envelope, complete piped output, and authenticated session issue/revoke lifecycle. Verify normal failures still revoke in finally.
  • Keep state discovery read-only; direct SQLite transcript reads should not be required to work around an HTTP crash.

pnpm check passes on the current repository commit f10a469681f91a65d2b22c713fb2fcd2004aba1a: 21 tests, typechecks, and build. The existing suite does not cover this failure. No source changes were made for this investigation.

Activity

  1. MajesteitBart commented on Sep 18, 2026

    @MajesteitBart
    Owner

    @ClarkvdM please fix

  2. ClarkvdM commented on Sep 18, 2026

    @ClarkvdM
    CollaboratorAuthor

    The fix is committed locally as 54403e6 on clark/issue-4-http-backpressure. It replaces bundled fetch for authenticated API calls with native HTTP/HTTPS, handles truncated bodies through T3_REQUEST_FAILED, and removes forced exit so large piped JSON finishes writing. pnpm check passes: 33 tests, typechecks, and build. Child-process backpressure tests and built-CLI session issue/revoke tests pass; independent code review found no blocking issues. No live Windows/T3 test was performed.

    Blocked on delivery: git push -u origin clark/issue-4-http-backpressure returned HTTP 403, "Permission to MajesteitBart/t3code-cli.git denied to ClarkvdM." No PR was created, and no settings or permissions were changed.

    Smallest action needed: grant ClarkvdM write access to this repository, or explicitly approve creating a fork and opening the PR from there. The work is preserved in /home/bartadmin/development/.clark-github/MajesteitBart/t3code-cli/issue-4.

  3. ClarkvdM commented on Sep 18, 2026

    @ClarkvdM
    CollaboratorAuthor

    Delivery unblocked: PR #5 is ready at f404fa6. Fresh pnpm check passes typechecks, all 34 tests and build; push and PR CI are green on Node 22.16.0 and 24.10.0. Initial CI exposed the existing SQLite warning in stderr; the tests now account for only that specific diagnostic and retain unexpected-error detection. No live Windows/desktop T3 qualification was performed. Nothing was merged or released.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions