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

BR
1mo ago
+182-3

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