Skip to content

▚▚ fix(http): treat end(callback) argument as a callback, not body data - #160

Open
breken-ai wants to merge 1 commit into
Use-Tusk:mainfrom
breken-ai:fix/http-end-callback-chunk
Open

breken-ai wants to merge 1 commit into
Use-Tusk:mainfrom
breken-ai:fix/http-end-callback-chunk

Conversation

@breken-ai

@breken-ai breken-ai commented Sep 14, 2026 •

Copy link
Copy Markdown

In RECORD mode, calling req.end(callback) on an instrumented outbound http/https request throws TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer ... Received function from the user's own call site, so the request never goes out. On the inbound side, a handler that ends with res.write(body); res.end(callback) still responds normally, but the recorded server span is exported without its response body and an error is logged. Both are the documented Node signature end([data[, encoding]][, callback]).

Root cause

The record-mode patches on ServerResponse.end (http/Instrumentation.ts) and ClientRequest.end (_captureClientRequestBody) only check if (chunk) before treating the first argument as body data. A callback-only end() therefore pushes the function into the chunk list:

  • server: combineChunks() calls Buffer.from(fn) inside the process.nextTick body processing; it throws, is caught and logged, and outputValue.body is never set.
  • client: Buffer.from(chunk, encoding) throws synchronously inside the patched req.end, propagating to the caller.

The client-side response end listener already guards with typeof chunk !== "function", and the replay-side TdMockClientRequest.end() handles the callback form correctly; the two record-mode end() patches (and the identical one in the Next.js instrumentation) were missed.

Fix

  • src/instrumentation/libraries/http/Instrumentation.ts: in the patched res.end and req.end, only capture chunk when typeof chunk !== "function". The argument is still passed through to the original end() unchanged, which already handles end(cb).
  • src/instrumentation/libraries/nextjs/Instrumentation.ts: same guard on the patched actualRes.end.

Tests

  • src/instrumentation/libraries/http/integration-tests/end-callback.test.ts (new, follows the existing in-memory-adapter integration pattern; requires http after TuskDrift.initialize() like small.test.ts so the RITM hook patches it):
    • inbound: handler does res.write(json); res.end(() => {}); asserts the SERVER span's outputValue.body decodes to the written JSON. Fails on main with outputValue.body === undefined.
    • outbound: inside an inbound span, an http.request is ended with req.end(() => {}); asserts the upstream call completes (200) and the CLIENT span's inputValue.body decodes to the request body. Fails on main with the ERR_INVALID_ARG_TYPE ... Received function error surfacing as a 500.
  • All src/instrumentation/libraries/http/**/*.test.ts pass (171 tests); tsc --noEmit and npm run build pass.

Not changed

  • res.write / req.write patches are untouched: write() has no callback-only form.
  • The Next.js guard is the same one-line change but is not exercised by a runtime test here (needs a Next.js app); it is covered by typecheck and build.

▚▚ Shipped by breken — self-healing software. This one's on us. breken.ai

Review in cubic

Node's documented signatures are response.end([data[, encoding]][, callback])
and request.end([data[, encoding]][, callback]). The record-mode patches on
ServerResponse.end and ClientRequest.end only checked `if (chunk)`, so a
callback-only end() pushed the function into the body chunk list.

Server side, combineChunks() then threw ERR_INVALID_ARG_TYPE inside the
nextTick body processing; the error was logged and the span was exported
without its response body, even when res.write() had already sent one.

Client side, Buffer.from(callback, encoding) threw synchronously out of the
user's own req.end(cb) call, so the outbound request never left the process
while recording was on.

Guard both patches (and the identical ServerResponse.end patch in the Next.js
instrumentation) with typeof chunk !== "function" and pass the argument through
to the original end() unchanged, which already handles the callback form.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

Re-trigger cubic

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant