Skip to content

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

Open
TheHypnoo wants to merge 3 commits into
mainfrom
fix/770-net-connect-options
Open

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

Conversation

@TheHypnoo
Copy link
Copy Markdown
Contributor

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): 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 (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) 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.
    • 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.

Test

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

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

Known limitations

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. Extending the dispatch table to accept three positional args is a follow-up.

Test plan

  • test-files/test_net_connect_options.ts runs locally on macOS arm64
  • User repro from net.connect(options, cb) options-object overload corrupts host string (NUL byte) #770 (net.connect({host: 'X', port: N}, cb)) connects successfully
  • 'error' listener receives object with usable .message on closed port
  • fetch / axios continue to return 200
  • Positional net.connect(port, host) keeps working
  • cargo test --release -p perry-ext-net -p perry-stdlib --lib passes
  • CI parity / cargo-test / compile-smoke green

TheHypnoo and others added 3 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.
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