fix: #770 — wire net.connect(options, cb) and emit Error objects#773
Merged
Conversation
…error'
`net.connect({ host, port }, cb)` — the standard Node.js options-object
overload — failed with "file name contained an unexpected NUL byte"
because the codegen dispatch entry only accepted the positional
`net.connect(port, host)` form. With `args: &[NA_F64, NA_STR]` the
second arg (the callback closure) was coerced through
`js_get_string_pointer_unified`, the runtime read garbage bytes from
the closure's memory as the hostname, and `getaddrinfo`'s internal
`CString::new()` blew up. Separately, the `'error'` event emitted raw
NaN-boxed strings instead of Error-shaped objects, so
`socket.on('error', err => err.message)` came back as `undefined`.
Reported in #767 (case 4) and tracked separately in #770.
Changes:
- `perry-codegen/src/lower_call.rs`: change `net.connect` /
`net.createConnection` `NativeModSig` from `&[NA_F64, NA_STR]` to
`&[NA_F64, NA_F64]` so the runtime sees both args' raw NaN-boxed
bits and can discriminate the overload by tag at runtime.
- `perry-ext-net/src/lib.rs` (and mirrored in
`perry-stdlib/src/net/mod.rs` for `PERRY_DISABLE_WELL_KNOWN=1`):
- `js_net_socket_connect(arg1_f64, arg2_f64)` now detects whether
`arg1` is a NaN-boxed object pointer (options form) or a regular
f64 number (positional form). Plain ports like `80.0` never carry
the `0x7FF8..=0x7FFF` quiet-NaN prefix, so the tag check is clean.
- For the options form, extract `host` / `hostname` / `port` from
the object via the runtime's `js_object_get_field_by_name_f64`
helper (perry-runtime in perry-stdlib; declared as `extern "C"`
from perry-ext-net since it doesn't depend on perry-runtime).
- Auto-register the optional `connectListener` (second arg in the
options form) as a `'connect'` listener on the new handle, matching
Node spec.
- `'error'` events now ship an Error-shaped object `{ message: msg }`
so `err.message` works. Falls back to a bare string on alloc
failure so the listener always receives *something*.
The positional `net.connect(port, host, cb)` form continues to work,
though the trailing `connectListener` arg is *not* auto-registered (the
dispatch entry only carries two arg slots); users should call
`socket.on('connect', cb)` or migrate to the options form.
Test: `test-files/test_net_connect_options.ts` exercises the options
form against an in-process `node:http` server (auto-registered
connectListener fires) and asserts the closed-port error fires with an
`{ message: string }` object. Verified locally on macOS arm64:
```
server listening
sock1: connectListener fired
sock2 error typeof: object
sock2 error message typeof: string
sock2 error has message: yes
done
```
`cargo test --release -p perry-ext-net -p perry-stdlib --lib` — all
green. Regression-checked `fetch` / `axios` (still work) and
positional `net.connect(port, host)` + `socket.on('connect', cb)`
(still works).
…ort, host, cb)
Follow-up on the same PR — the previous commit only auto-registered the
optional connectListener in the options-object form. The positional
3-arg form `net.connect(port, host, cb)` is also part of the spec'd
Node API and should fire `cb` on connect without requiring an explicit
`socket.on('connect', cb)`.
- Codegen dispatch entries for `net.connect` / `net.createConnection`
now declare a third `NA_F64` slot. The dispatch loop pads missing
user args with `TAG_UNDEFINED`, so the 2-arg call sites keep working.
- `js_net_socket_connect(arg1, arg2, arg3)` now wires the
`connectListener` from whichever slot it lands in:
- options form → arg2
- positional → arg3
Mirrored in perry-stdlib's `bundled-net` path for
`PERRY_DISABLE_WELL_KNOWN=1` parity.
- `is_nanboxed_pointer` (renamed from `looks_like_nanboxed_object`)
tightened to match `POINTER_TAG` (0x7FFD) only — `undefined` (0x7FFC)
now reliably falls through, which matters because the dispatch table
pads missing user args with `TAG_UNDEFINED`.
- Manifest entries for `net.connect` / `net.createConnection` updated
to declare three params (drift guard), and docs/api regenerated.
- Test fixture extended to cover the positional + cb form.
Verified locally on macOS arm64:
```
sock1 (options): connectListener fired
sock2 (positional): connectListener fired
sock3 error typeof: object
sock3 error message typeof: string
```
`cargo test --release -p perry-codegen --test manifest_consistency` —
all four assertions green.
CI on #773 surfaced two issues: - lint: rustfmt collapsed the multi-line `js_net_socket_connect` signature to a single line in both perry-ext-net and perry-stdlib. - parity: \`test_net_connect_options.ts\` opened sock1 and sock2 in parallel, so the two \"connectListener fired\" prints raced — output order varied run-to-run and diverged from Node's. Sequenced the connections (sock2 inside sock1's listener, sock3 inside sock2's) so the line order is deterministic and matches \`node --experimental-strip-types\`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #770 (split off from #767 case 4).
net.connect({ host, port }, cb)— the standard Node.js options-object overload — failed with"file name contained an unexpected NUL byte"because the codegen dispatch entry only accepted the positionalnet.connect(port, host)form. Withargs: &[NA_F64, NA_STR]the second arg (the callback closure) was coerced throughjs_get_string_pointer_unified, the runtime read garbage bytes from the closure's memory as the hostname, andgetaddrinfo's internalCString::new()blew up.Separately, the
'error'event emitted raw NaN-boxed strings instead of Error-shaped objects, sosocket.on('error', err => err.message)came back asundefined.What changed
crates/perry-codegen/src/lower_call.rs):net.connect/net.createConnectionnow declareargs: &[NA_F64, NA_F64, NA_F64](3 slots, both overloads supported). The runtime sees raw NaN-boxed bits and discriminates by tag.perry-ext-net(crates/perry-ext-net/src/lib.rs) andperry-stdlib(crates/perry-stdlib/src/net/mod.rs, for thePERRY_DISABLE_WELL_KNOWN=1path):js_net_socket_connect(arg1_f64, arg2_f64, arg3_f64)detects whetherarg1is a NaN-boxed object pointer (POINTER_TAG=0x7FFD— narrow check that cleanly rejectsundefinedpadding) or a regular f64 number. Plain ports like80.0never carry the quiet-NaN prefix.host/hostname/portfrom the object via the runtime'sjs_object_get_field_by_name_f64.connectListenerin both forms:net.connect({host, port}, cb)→cbcomes througharg2net.connect(port, host, cb)→cbcomes througharg3'error'events now ship an Error-shaped object{ message: msg }soerr.messageworks. Falls back to a bare string on alloc failure.crates/perry-api-manifest/src/entries.rs): widenednet.connect/net.createConnectionto declare 3 params to satisfy the dispatch/manifest drift guard, plus regenerateddocs/api/perry.d.ts.Test
New fixture
test-files/test_net_connect_options.tschains sock1 (options) → sock2 (positional) → sock3 (closed-port error) so parity output order is deterministic againstnode --experimental-strip-types:cargo test --release -p perry-codegen --test manifest_consistency -p perry-ext-net -p perry-stdlib --lib— all green. Regression-checkedfetch/axios(still work) and the originalnet.connect(port, host)positional form (still works).Test plan
test-files/test_net_connect_options.tsmatches Node's output byte-for-bytenet.connect({host: 'X', port: N}, cb)) connects and fires the connectListenernet.connect(port, host, cb)also fires the connectListener'error'listener receives object with usable.messageon closed portfetch/axioscontinue to return200cargo test --release -p perry-codegen --test manifest_consistency -p perry-ext-net -p perry-stdlib --libpasses