Skip to content

MockHttpSocket.passthrough() can cause rare EINVAL, ECANCELED errorsΒ #753

@jbinto

Description

@jbinto

We use dd-trace to instrument our test suite. Since upgrading to nock@14 (or [email protected]), which now uses @mswjs/[email protected], we've been having issues where the periodic dd-trace telemetry uploads are crashing the process with either EINVAL or ECANCELED. They are allowed to passthrough, but there is something about the implementation in MockHttpSocket such that in a large test suite this will always happen to multiple tests, but it's rare enough that reproducing it once at will is annoyingly difficult.

ECANCELED (failed write)

Error: write ECANCELED Canceled because of SSL destruction
    at WriteWrap.onWriteComplete [as oncomplete] (node:internal/stream_base_commons:95:16)
    at WriteWrap.callbackTrampoline (node:internal/async_hooks:130:17)
    at TCP.done (node:_tls_wrap:641:11)
    at TCP.callbackTrampoline (node:internal/async_hooks:130:17)
Emitted 'error' event on ClientRequest instance at:
    at ClientRequest.req.emit (/home/circleci/project/node_modules/dd-trace/packages/datadog-instrumentations/src/http/client.js:123:25)
    at emitErrorEvent (node:_http_client:101:11)
    at MockHttpSocket.socketErrorListener (node:_http_client:504:5)
    at MockHttpSocket.emit (node:events:536:35)
    at MockHttpSocket.emit (/home/circleci/project/node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/MockHttpSocket.ts:160:12)
    at TLSSocket.<anonymous> (/home/circleci/project/node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/MockHttpSocket.ts:293:14)
    at TLSSocket.emit (node:events:524:28)
    at TLSSocket.emit (/home/circleci/project/node_modules/dd-trace/packages/datadog-instrumentations/src/net.js:65:25)
    at emitErrorNT (node:internal/streams/destroy:169:8)
    at emitErrorCloseNT (node:internal/streams/destroy:128:3)
    at processTicksAndRejections (node:internal/process/task_queues:82:21) {
  errno: -125,
  code: 'ECANCELED',
  syscall: 'write'
}

EINVAL (failed read)

Error: read EINVAL
    at tryReadStart (node:net:716:20)
    at Socket._read (node:net:731:5)
    at MockHttpSocket.<anonymous> (node:net:729:37)
    at Object.onceWrapper (node:events:633:28)
    at MockHttpSocket.emit (node:events:531:35)
    at MockHttpSocket.emit (/home/circleci/interceptors/repro/node_modules/.pnpm/@[email protected]/node_modules/@mswjs/interceptors/lib/node/chunk-N4ZZFE24.js:454:12)
    at TLSSocket.<anonymous> (/home/circleci/interceptors/repro/node_modules/.pnpm/@[email protected]/node_modules/@mswjs/interceptors/lib/node/chunk-N4ZZFE24.js:531:12)
    at TLSSocket.emit (node:events:531:35)
    at /home/circleci/interceptors/repro/node_modules/.pnpm/[email protected]_@[email protected]_@[email protected]_@[email protected]_/node_modules/dd-trace/packages/datadog-instrumentations/src/net.js:62:27
    at run (node:diagnostics_channel:168:14)
Emitted 'error' event on ClientRequest instance at:
    at req.emit (/home/circleci/interceptors/repro/node_modules/.pnpm/[email protected]_@[email protected]_@[email protected]_@[email protected]_/node_modules/dd-trace/packages/datadog-instrumentations/src/http/client.js:123:25)
    at emitErrorEvent (node:_http_client:107:11)
    at MockHttpSocket.socketErrorListener (node:_http_client:574:5)
    at MockHttpSocket.emit (node:events:519:28)
    at MockHttpSocket.emit (/home/circleci/interceptors/repro/node_modules/.pnpm/@[email protected]/node_modules/@mswjs/interceptors/lib/node/chunk-N4ZZFE24.js:454:12)
    at TLSSocket.<anonymous> (/home/circleci/interceptors/repro/node_modules/.pnpm/@[email protected]/node_modules/@mswjs/interceptors/lib/node/chunk-N4ZZFE24.js:536:12)
    at TLSSocket.emit (node:events:519:28)
    at TLSSocket.emit (/home/circleci/interceptors/repro/node_modules/.pnpm/[email protected]_@[email protected]_@[email protected]_@[email protected]_/node_modules/dd-trace/packages/datadog-instrumentations/src/net.js:65:25)
    at emitErrorNT (node:internal/streams/destroy:170:8)
    at emitErrorCloseNT (node:internal/streams/destroy:129:3) {
  errno: -22,
  code: 'EINVAL',
  syscall: 'read'
}

Potential solutions

If I revert the changes in #706 99967d7 the issue goes away. I understand that was in place for a reason, but reverting absolutely solves this. It seems like having the _handle exposed gets dicey when something else has closed the socket and (consumers of) MockHttpSocket thinks it's still ok to read/write.

I'm not well versed in this (type of) codebase. I rarely think about this low level socket stuff. I had an LLM hold my hand through the process of generating detailed logs, reproductions. I got very far with very little knowledge, which is a dangerous thing ;)

It's suggesting:

  public _read(size: number): void {
    if (this.socketState === 'passthrough') {
      return
    }

    super._read(size)
  }

and then removing the '_handle' aliasing. It suggests that the repeated _read is the source of the event listener oversubscription, and removing _handle stops the "2 owners of a single handle" issue.

This seems to work well. I'm happy to submit a PR, but I wanted to socialize it first, and also make sure it works well for us in production for a bit. Reproducing in an isolated repo has been very hard: I was successful exactly 2 times (out of hundreds of attempts) using the real dd-trace. Trying to simulate its behavior with an identical http.agent did not work. (But of course in our production apps, this happens dozens of times per CI run... 😬)

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