Skip to content

fix: #770 — wire net.connect(options, cb) and emit Error objects#773

Merged
TheHypnoo merged 5 commits into
mainfrom
fix/770-net-connect-options
May 14, 2026
Merged

fix: #770 — wire net.connect(options, cb) and emit Error objects#773
TheHypnoo merged 5 commits into
mainfrom
fix/770-net-connect-options

Conversation

@TheHypnoo
Copy link
Copy Markdown
Contributor

@TheHypnoo TheHypnoo commented May 14, 2026

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 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.

What changed

  • Codegen dispatch (crates/perry-codegen/src/lower_call.rs): net.connect / net.createConnection now declare args: &[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) and perry-stdlib (crates/perry-stdlib/src/net/mod.rs, for the PERRY_DISABLE_WELL_KNOWN=1 path):
    • js_net_socket_connect(arg1_f64, arg2_f64, arg3_f64) detects whether arg1 is a NaN-boxed object pointer (POINTER_TAG = 0x7FFD — narrow check that cleanly rejects undefined padding) or a regular f64 number. Plain ports like 80.0 never carry the quiet-NaN prefix.
    • For the options form, extract host / hostname / port from the object via the runtime's js_object_get_field_by_name_f64.
    • Auto-register the optional connectListener in both forms:
      • net.connect({host, port}, cb)cb comes through arg2
      • net.connect(port, host, cb)cb comes through arg3
    • 'error' events now ship an Error-shaped object { message: msg } so err.message works. Falls back to a bare string on alloc failure.
  • Manifest (crates/perry-api-manifest/src/entries.rs): widened net.connect / net.createConnection to declare 3 params to satisfy the dispatch/manifest drift guard, plus regenerated docs/api/perry.d.ts.

Test

New fixture test-files/test_net_connect_options.ts chains sock1 (options) → sock2 (positional) → sock3 (closed-port error) so parity output order is deterministic against node --experimental-strip-types:

server listening
sock1 (options): connectListener fired
sock2 (positional): connectListener fired
sock3 error typeof: object
sock3 error message typeof: string
sock3 error has message: yes
done

cargo test --release -p perry-codegen --test manifest_consistency -p perry-ext-net -p perry-stdlib --lib — all green. Regression-checked fetch / axios (still work) and the original net.connect(port, host) positional form (still works).

Test plan

  • test-files/test_net_connect_options.ts matches Node's output byte-for-byte
  • User repro from net.connect(options, cb) options-object overload corrupts host string (NUL byte) #770 (net.connect({host: 'X', port: N}, cb)) connects and fires the connectListener
  • net.connect(port, host, cb) also fires the connectListener
  • 'error' listener receives object with usable .message on closed port
  • fetch / axios continue to return 200
  • cargo test --release -p perry-codegen --test manifest_consistency -p perry-ext-net -p perry-stdlib --lib passes
  • CI parity / cargo-test / lint / compile-smoke green

TheHypnoo and others added 5 commits May 14, 2026 16:54
…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\`.
@TheHypnoo TheHypnoo merged commit 5a4f217 into main May 14, 2026
9 checks passed
@TheHypnoo TheHypnoo deleted the fix/770-net-connect-options branch May 14, 2026 19:18
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.

net.connect(options, cb) options-object overload corrupts host string (NUL byte)

1 participant