-
Notifications
You must be signed in to change notification settings - Fork 29.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
error caught, but exitCode is not zero #48756
Comments
Quoting the issue template:
You say "what" but not "why". I've looked at your example and it doesn't explain it either. |
Basically, the example is something like below try {
await fn(testOptionFromMatrix)
} catch(e) {} I think nobody expect above code exitCode is not zero, so does not need explaination. Besides,
Current behavior is also not identical. If current behavior is expected, please provide code, showing how to get |
What exceptions / error messages do the two edit: edited for clarity |
full code/* global console */
import { deepStrictEqual } from 'node:assert'
import { spawnSync } from 'node:child_process'
import { request } from 'node:http'
import { env, nextTick } from 'node:process'
import { inspect } from 'node:util'
import { isMainThread } from 'node:worker_threads'
import { describe, it } from 'node:test'
// Env TEST_CONTAINER_IP with a v4 ip,
// with http server on its 80 port.
// During the test,
// this ip is unreachable.
const TEST_CONTAINER_IP = env['TEST_CONTAINER_IP']
function simulateBadNetwork(code: '' | 'icmp-host-unreachable' | 'blackhole', ip: string, want: boolean): void {
switch (code) {
case 'icmp-host-unreachable':
if (want) {
spawnSync('sudo', [
'iptables',
'-A',
'OUTPUT',
'-d',
`${ip}/32`,
'-m',
'comment',
'--comment',
'x_test',
'-j',
'REJECT',
'--reject-with',
'icmp-host-unreachable',
])
} else {
spawnSync('sudo', [
'iptables',
'-D',
'OUTPUT',
'-d',
`${ip}/32`,
'-m',
'comment',
'--comment',
'x_test',
'-j',
'REJECT',
'--reject-with',
'icmp-host-unreachable',
])
}
break
case 'blackhole':
if (want) {
spawnSync('sudo', ['ip', 'route', 'add', 'blackhole', `${ip}`])
} else {
spawnSync('sudo', ['ip', 'route', 'del', 'blackhole', `${ip}`])
}
break
default:
break
}
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
describe(request.name, { concurrency: true }, () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
it(request.name, { concurrency: true }, async () => {
interface WorkerData {
ip: string
code: Parameters<typeof simulateBadNetwork>[0]
nextTickLookup: boolean
}
if (isMainThread) {
const { Worker } = await import('node:worker_threads')
async function getExitCode(workerData: WorkerData): Promise<{ caught: boolean; exitCode: number }> {
return await new Promise((resolve, _reject) => {
let caught = false
const msg: any[] = []
const worker = new Worker(new URL(import.meta.url), { workerData, stdout: true, stderr: true })
worker.on('message', (value) => {
caught = true
msg.push(value)
})
worker.on('exit', (exitCode) => {
simulateBadNetwork(workerData.code, workerData.ip, false)
resolve({ caught, exitCode })
console.warn({ workerData, caught, exitCode, msg })
})
})
}
if (TEST_CONTAINER_IP === undefined) throw new Error()
const ip = TEST_CONTAINER_IP
deepStrictEqual(
await getExitCode({ ip, code: '', nextTickLookup: false }),
{ caught: false, exitCode: 0 },
)
deepStrictEqual(
await getExitCode({ ip, code: 'icmp-host-unreachable', nextTickLookup: false }),
{ caught: true, exitCode: 0 },
)
deepStrictEqual(
await getExitCode({ ip, code: 'blackhole', nextTickLookup: false }),
// unexpected
{ caught: true, exitCode: 1 },
)
deepStrictEqual(
await getExitCode({ ip, code: '', nextTickLookup: true }),
{ caught: false, exitCode: 0 },
)
deepStrictEqual(
await getExitCode({ ip, code: 'icmp-host-unreachable', nextTickLookup: true }),
{ caught: true, exitCode: 0 },
)
deepStrictEqual(
await getExitCode({ ip, code: 'blackhole', nextTickLookup: true }),
{ caught: true, exitCode: 0 },
)
} else {
const workerData = (await import('node:worker_threads')).workerData as WorkerData
const { parentPort } = await import('node:worker_threads')
try {
await new Promise<void>((resolve, reject) => {
const clientRequest = request('http://example.com', {
lookup(_hostname, _options, callback) {
if (workerData.nextTickLookup) {
nextTick(() => {
simulateBadNetwork(workerData.code, workerData.ip, true)
callback(null, [{ address: workerData.ip, family: 4 }])
})
} else {
simulateBadNetwork(workerData.code, workerData.ip, true)
callback(null, [{ address: workerData.ip, family: 4 }])
}
},
})
const cleanup = (): void => {
clientRequest.removeListener('error', onError)
clientRequest.removeListener('close', onClose)
}
const onError = (err: Error): void => {
cleanup()
reject(new Error(undefined, { cause: err }))
}
const onClose = (): void => {
cleanup()
resolve()
}
clientRequest.addListener('error', onError)
clientRequest.addListener('close', onClose)
clientRequest.end()
})
} catch (e) {
parentPort?.postMessage(inspect(e))
}
}
})
}) full log(node:413482) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
TAP version 13
{
workerData: { ip: '{{TEST_CONTAINER_IP}}', code: '', nextTickLookup: false },
caught: false,
exitCode: 0,
msg: []
}
{
workerData: {
ip: '{{TEST_CONTAINER_IP}}',
code: 'icmp-host-unreachable',
nextTickLookup: false
},
caught: true,
exitCode: 0,
msg: [
'Error\n' +
' at ClientRequest.onError (/path/to/reproduce.ts:154:20)\n' +
' at ClientRequest.emit (node:events:512:28)\n' +
' at Socket.socketErrorListener (node:_http_client:495:9)\n' +
' at Socket.emit (node:events:512:28)\n' +
' at emitErrorNT (node:internal/streams/destroy:151:8)\n' +
' at emitErrorCloseNT (node:internal/streams/destroy:116:3)\n' +
' at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {\n' +
' [cause]: Error: connect EHOSTUNREACH {{TEST_CONTAINER_IP}}:80\n' +
' at __node_internal_captureLargerStackTrace (node:internal/errors:496:5)\n' +
' at __node_internal_exceptionWithHostPort (node:internal/errors:671:12)\n' +
' at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1592:16) {\n' +
' errno: -113,\n' +
" code: 'EHOSTUNREACH',\n" +
" syscall: 'connect',\n" +
" address: '{{TEST_CONTAINER_IP}}',\n" +
' port: 80\n' +
' }\n' +
'}'
]
}
{
workerData: { ip: '{{TEST_CONTAINER_IP}}', code: 'blackhole', nextTickLookup: false },
caught: true,
exitCode: 1,
msg: [
'Error\n' +
' at ClientRequest.onError (/path/to/reproduce.ts:154:20)\n' +
' at ClientRequest.emit (node:events:512:28)\n' +
' at Socket.socketCloseListener (node:_http_client:468:11)\n' +
' at Socket.emit (node:events:524:35)\n' +
' at TCP.<anonymous> (node:net:334:12) {\n' +
' [cause]: Error: socket hang up\n' +
' at connResetException (node:internal/errors:720:14)\n' +
' at Socket.socketCloseListener (node:_http_client:468:25)\n' +
' at Socket.emit (node:events:524:35)\n' +
' at TCP.<anonymous> (node:net:334:12) {\n' +
" code: 'ECONNRESET'\n" +
' }\n' +
'}'
]
}
{
workerData: { ip: '{{TEST_CONTAINER_IP}}', code: '', nextTickLookup: true },
caught: false,
exitCode: 0,
msg: []
}
{
workerData: {
ip: '{{TEST_CONTAINER_IP}}',
code: 'icmp-host-unreachable',
nextTickLookup: true
},
caught: true,
exitCode: 0,
msg: [
'Error\n' +
' at ClientRequest.onError (/path/to/reproduce.ts:154:20)\n' +
' at ClientRequest.emit (node:events:512:28)\n' +
' at Socket.socketErrorListener (node:_http_client:495:9)\n' +
' at Socket.emit (node:events:512:28)\n' +
' at emitErrorNT (node:internal/streams/destroy:151:8)\n' +
' at emitErrorCloseNT (node:internal/streams/destroy:116:3)\n' +
' at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {\n' +
' [cause]: Error: connect EHOSTUNREACH {{TEST_CONTAINER_IP}}:80\n' +
' at __node_internal_captureLargerStackTrace (node:internal/errors:496:5)\n' +
' at __node_internal_exceptionWithHostPort (node:internal/errors:671:12)\n' +
' at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1592:16) {\n' +
' errno: -113,\n' +
" code: 'EHOSTUNREACH',\n" +
" syscall: 'connect',\n" +
" address: '{{TEST_CONTAINER_IP}}',\n" +
' port: 80\n' +
' }\n' +
'}'
]
}
{
workerData: { ip: '{{TEST_CONTAINER_IP}}', code: 'blackhole', nextTickLookup: true },
caught: true,
exitCode: 0,
msg: [
'Error\n' +
' at ClientRequest.onError (/path/to/reproduce.ts:154:20)\n' +
' at ClientRequest.emit (node:events:512:28)\n' +
' at Socket.socketErrorListener (node:_http_client:495:9)\n' +
' at Socket.emit (node:events:512:28)\n' +
' at emitErrorNT (node:internal/streams/destroy:151:8)\n' +
' at emitErrorCloseNT (node:internal/streams/destroy:116:3)\n' +
' at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {\n' +
' [cause]: Error: connect EINVAL {{TEST_CONTAINER_IP}}:80 - Local (0.0.0.0:0)\n' +
' at __node_internal_captureLargerStackTrace (node:internal/errors:496:5)\n' +
' at __node_internal_exceptionWithHostPort (node:internal/errors:671:12)\n' +
' at internalConnect (node:net:1087:16)\n' +
' at defaultTriggerAsyncIdScope (node:internal/async_hooks:464:18)\n' +
' at emitLookup (node:net:1478:9)\n' +
' at <anonymous> (/path/to/reproduce.ts:138:19)\n' +
' at process.processTicksAndRejections (node:internal/process/task_queues:77:11) {\n' +
' errno: -22,\n' +
" code: 'EINVAL',\n" +
" syscall: 'connect',\n" +
" address: '{{TEST_CONTAINER_IP}}',\n" +
' port: 80\n' +
' }\n' +
'}'
]
}
# Subtest: request
# Subtest: request
ok 1 - request
---
duration_ms: 1086.378818
...
1..1
ok 1 - request
---
duration_ms: 1087.921034
type: 'suite'
...
1..1
# tests 1
# suites 1
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 0.215729 |
This here is likely your problem. It gets kind of obfuscated due to the stitched-together async stack trace but that's a synchronous "bad parameter" exception, not a runtime network error like the others. |
That one is caught: true,
exitCode: 0,
msg: [
...
' [cause]: Error: connect EINVAL {{TEST_CONTAINER_IP}}:80 - Local (0.0.0.0:0)\n' + The problematice one {
workerData: { ip: '{{TEST_CONTAINER_IP}}', code: 'blackhole', nextTickLookup: false },
caught: true,
exitCode: 1,
msg: [
'Error\n' +
' at ClientRequest.onError (/path/to/reproduce.ts:154:20)\n' +
' at ClientRequest.emit (node:events:512:28)\n' +
' at Socket.socketCloseListener (node:_http_client:468:11)\n' +
' at Socket.emit (node:events:524:35)\n' +
' at TCP.<anonymous> (node:net:334:12) {\n' +
' [cause]: Error: socket hang up\n' +
' at connResetException (node:internal/errors:720:14)\n' +
' at Socket.socketCloseListener (node:_http_client:468:25)\n' +
' at Socket.emit (node:events:524:35)\n' +
' at TCP.<anonymous> (node:net:334:12) {\n' +
" code: 'ECONNRESET'\n" +
' }\n' +
'}'
]
} |
Even more weird. Extract code from worker based test, now error is not caught, which does caught within worker. I may create a reproduce repo when have time, seems like a complicated problem. code not using workerimport { spawnSync } from 'node:child_process'
import { request } from 'node:http'
import { env, nextTick } from 'node:process'
import { inspect } from 'node:util'
const TEST_CONTAINER_IP = env['TEST_CONTAINER_IP']
function simulateBadNetwork(code: '' | 'icmp-host-unreachable' | 'blackhole', ip: string, want: boolean): void {
switch (code) {
case 'icmp-host-unreachable':
if (want) {
spawnSync('sudo', [
'iptables',
'-A',
'OUTPUT',
'-d',
`${ip}/32`,
'-m',
'comment',
'--comment',
'x_test',
'-j',
'REJECT',
'--reject-with',
'icmp-host-unreachable',
])
} else {
spawnSync('sudo', [
'iptables',
'-D',
'OUTPUT',
'-d',
`${ip}/32`,
'-m',
'comment',
'--comment',
'x_test',
'-j',
'REJECT',
'--reject-with',
'icmp-host-unreachable',
])
}
break
case 'blackhole':
if (want) {
spawnSync('sudo', ['ip', 'route', 'add', 'blackhole', `${ip}`])
} else {
spawnSync('sudo', ['ip', 'route', 'del', 'blackhole', `${ip}`])
}
break
default:
break
}
}
{
interface WorkerData {
ip: string
code: Parameters<typeof simulateBadNetwork>[0]
nextTickLookup: boolean
}
if (TEST_CONTAINER_IP === undefined) throw new Error()
const ip = TEST_CONTAINER_IP
const workerData: WorkerData = { ip, code: 'blackhole', nextTickLookup: false }
try {
await new Promise<void>((resolve, reject) => {
const clientRequest = request('http://example.com', {
lookup(_hostname, _options, callback) {
if (workerData.nextTickLookup) {
nextTick(() => {
simulateBadNetwork(workerData.code, workerData.ip, true)
callback(null, [{ address: workerData.ip, family: 4 }])
})
} else {
simulateBadNetwork(workerData.code, workerData.ip, true)
callback(null, [{ address: workerData.ip, family: 4 }])
}
},
})
const cleanup = (): void => {
clientRequest.removeListener('error', onError)
clientRequest.removeListener('close', onClose)
}
const onError = (err: Error): void => {
cleanup()
reject(new Error(undefined, { cause: err }))
}
const onClose = (): void => {
cleanup()
resolve()
}
clientRequest.addListener('error', onError)
clientRequest.addListener('close', onClose)
clientRequest.end()
})
} catch (e) {
console.warn({ workerData, caught: true, msg: [inspect(e)] })
}
} log not using worker(node:452690) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
node:events:490
throw er; // Unhandled 'error' event
^
Error: connect EINVAL {{TEST_CONTAINER_IP}}:80 - Local (0.0.0.0:0)
at __node_internal_captureLargerStackTrace (node:internal/errors:496:5)
at __node_internal_exceptionWithHostPort (node:internal/errors:671:12)
at internalConnect (node:net:1087:16)
at defaultTriggerAsyncIdScope (node:internal/async_hooks:464:18)
at emitLookup (node:net:1478:9)
at lookup (/path/to/reproduce.ts:80:13)
at emitLookup (node:net:1402:5)
at defaultTriggerAsyncIdScope (node:internal/async_hooks:464:18)
at lookupAndConnectMultiple (node:net:1401:3)
at node:net:1347:7
at defaultTriggerAsyncIdScope (node:internal/async_hooks:464:18)
at lookupAndConnect (node:net:1346:5)
at Socket.connect (node:net:1243:5)
at Agent.connect [as createConnection] (node:net:233:17)
at Agent.createSocket (node:_http_agent:342:26)
at Agent.addRequest (node:_http_agent:289:10)
at new ClientRequest (node:_http_client:337:16)
at request (node:http:101:10)
at console.warn.workerData.workerData (/path/to/reproduce.ts:71:29)
at new Promise (<anonymous>)
at <anonymous> (/path/to/reproduce.ts:70:11)
at ModuleJob.run (node:internal/modules/esm/module_job:192:25)
at async CustomizedModuleLoader.import (node:internal/modules/esm/loader:228:24)
at async loadESM (node:internal/process/esm_loader:40:7)
at async handleMainPromise (node:internal/modules/run_main:66:12)
Emitted 'error' event on Socket instance at:
at emitErrorNT (node:internal/streams/destroy:151:8)
at emitErrorCloseNT (node:internal/streams/destroy:116:3)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
errno: -22,
code: 'EINVAL',
syscall: 'connect',
address: '{{TEST_CONTAINER_IP}}',
port: 80
}
Node.js v20.4.0 |
I suggest nodejs wrap If so, all six test cases should be catch-able. If not,
I use |
Oh, I know what you mean, I replace them, not node. I think better for reading. |
Close in flavor of smaller reproduce. |
Version
20.4.0
Platform
Docker ArchLinux 6.1.35-1-lts
Subsystem
No response
What steps will reproduce the bug?
full code
How often does it reproduce? Is there a required condition?
What is the expected behavior? Why is that the expected behavior?
What do you see instead?
Additional information
No response
The text was updated successfully, but these errors were encountered: