Summary
Perry's built-in HTTP client modules (fetch, axios, node:http, net) are unable to establish any network connections — including to localhost and direct IP addresses — when compiled to a native binary on macOS arm64. The child_process module works correctly, and external tools like curl can make network requests without issues, confirming the host machine has full network connectivity.
Environment
| Item |
Value |
| Perry Version |
0.5.891 (also tested: 0.5.511, 0.5.890 — same result) |
| OS |
macOS 26.4.1 (Darwin) |
| Architecture |
arm64 (Apple Silicon) |
| Perry Package |
@perryts/perry-darwin-arm64 0.5.891 |
| Xcode CLI Tools |
Installed |
| Network |
Fully functional (verified via system curl) |
Steps to Reproduce
Case 1: fetch fails
// fetch_test.ts
const response = await fetch("https://api.ipify.org?format=json");
console.log(response.status);
perry compile fetch_test.ts -o fetch_test && ./fetch_test
# Expected: 200
# Actual: Fetch error: error sending request for url (https://api.ipify.org/?format=json)
Case 2: axios fails
// axios_test.ts
import axios from "axios"
const res = await axios.get("https://api.ipify.org?format=json");
console.log(res.status);
perry compile axios_test.ts -o axios_test && ./axios_test
# Expected: 200
# Actual: Request failed: error sending request for url (https://api.ipify.org/?format=json)
Case 3: node:http client returns undefined
// http_test.ts
import { request } from "node:http"
const req = request("http://1.1.1.1/", (res) => { console.log(res.statusCode); });
req.on("error", (err) => { console.error(err); });
req.end();
perry compile http_test.ts -o http_test && ./http_test
# Expected: 200
# Actual: TypeError: Cannot read properties of undefined (reading 'on')
Case 4: net.connect fails
// net_test.ts
import * as net from "net"
const socket = net.connect({ host: "1.1.1.1", port: 80 }, () => { console.log("connected"); });
socket.on("error", (err) => { console.error(err.message); });
perry compile net_test.ts -o net_test && ./net_test
# Expected: "connected"
# Actual: TCP error: file name contained an unexpected NUL byte
Note: The error "file name contained an unexpected NUL byte" is a Rust CString::new() error, indicating that the hostname string passed to the underlying socket API contains NUL (\0) bytes. This strongly suggests a string handling bug in Perry's net module FFI bridge, where TypeScript strings are not properly converted to C-style strings before being passed to system calls.
Case 5: Direct IP and localhost also fail
await fetch("http://1.1.1.1/"); // → error sending request
await fetch("http://127.0.0.1:9999/"); // → error sending request
This rules out DNS resolution as the root cause.
Diagnostic Evidence
child_process + curl works correctly
import { execSync } from "child_process"
const result = execSync("curl -s https://api.ipify.org?format=json", { encoding: "utf8" });
console.log(result); // → {"ip":"x.x.x.x"} ✅
This confirms:
- The host machine has full network connectivity
- DNS resolution works at the OS level
- TLS/HTTPS works via system curl
- The compiled Perry binary can spawn child processes
Compilation output confirms stdlib is linked
Collecting modules...
Found 1 module(s): 1 native, 0 JavaScript ← correctly detected as native
Generating code...
Linking (with stdlib)... ← stdlib linked (includes http-client)
Binary size: 7.2MB ← consistent with stdlib-included binary
Compare with runtime-only linking (no fetch):
Linking (runtime-only)...
Binary size: 1.1MB
perry dev mode also fails
The same fetch code fails identically when run via perry dev, ruling out a compile-only issue.
RUST_LOG=debug produces no additional output
No networking-related debug messages are emitted, suggesting the error occurs before any network I/O is attempted (possibly in the runtime initialization or FFI bridge).
perry --print-api-manifest confirms APIs are registered
child_process.spawn ✅ listed
child_process.execSync ✅ listed
fs.readFileSync ✅ listed
fs.statSync ✅ listed
The HTTP client modules (fetch, axios, node:http, net) are also present in the manifest and linked.
Comparison with Known Issues
Workaround
Using child_process + curl as an HTTP transport proxy:
- Non-streaming:
execSync("curl -s ...") — works synchronously
- Streaming:
spawn("curl", ["-s", "-N", "-o", tmpFile, ...]) + file polling with setInterval + fs.readFileSync — works for SSE streaming
This workaround is functional but has drawbacks:
- Requires
curl to be available on the target system
- Adds overhead from process spawning and file I/O
- Streaming requires a temp file and polling (not event-driven)
Minimal Reproduction
// repro.ts — simplest possible reproduction
try {
const res = await fetch("http://1.1.1.1/");
console.log("OK:", res.status);
} catch (err: any) {
console.error("FAIL:", err.message);
}
perry compile repro.ts -o repro && ./repro
# Output: FAIL: Fetch error: error sending request for url (http://1.1.1.1/)
Suggested Investigation Areas
- String handling in FFI bridge: The
net.connect error "file name contained an unexpected NUL byte" indicates TypeScript strings are not properly converted to C-style strings (CString) before being passed to socket APIs. This may also affect fetch/axios internally when resolving hostnames.
- Tokio runtime initialization: Check if the async runtime is properly initialized for network I/O in CLI (non-UI) compiled binaries
- Hyper/rustls client configuration: Verify the HTTP client backend (hyper + rustls) is correctly wired for outbound connections
- Error propagation: The
node:http request() returning undefined and net.connect previously returning "undefined" error both suggest the FFI error chain is broken — errors from Rust are not properly propagated to TypeScript
Environment Exclusion
The following external causes have been ruled out:
- macOS Firewall: Disabled (
socketfilterfw --getglobalstate → State = 0)
- Binary sandbox/entitlements: Ad-hoc signed, no sandbox (
codesign -dvvv → flags=0x20002(adhoc,linker-signed), no runtime flag)
- DNS issues: Direct IP connections also fail, ruling out DNS
- TLS issues: Plain HTTP connections also fail, ruling out TLS
- System network:
child_process + curl works from the same binary, confirming host network is functional
- Version-specific regression: Tested across 3 versions (0.5.511, 0.5.890, 0.5.891) — all exhibit identical failures, confirming this is a systemic issue, not a regression
Cross-Version Test Results
| Perry Version |
Binary Size |
fetch |
axios |
net.connect |
node:http |
| 0.5.511 |
5.4MB |
FAIL |
FAIL |
FAIL (NUL byte) |
FAIL (undefined) |
| 0.5.890 |
7.3MB |
FAIL |
FAIL |
FAIL (NUL byte) |
FAIL (undefined) |
| 0.5.891 |
7.3MB |
FAIL |
FAIL |
FAIL (NUL byte) |
FAIL (undefined) |
All versions produce identical error messages, indicating the HTTP client stack has never worked on macOS arm64.
Summary
Perry's built-in HTTP client modules (
fetch,axios,node:http,net) are unable to establish any network connections — including tolocalhostand direct IP addresses — when compiled to a native binary on macOS arm64. Thechild_processmodule works correctly, and external tools likecurlcan make network requests without issues, confirming the host machine has full network connectivity.Environment
@perryts/perry-darwin-arm640.5.891curl)Steps to Reproduce
Case 1:
fetchfailsCase 2:
axiosfailsCase 3:
node:httpclient returns undefinedCase 4:
net.connectfailsCase 5: Direct IP and localhost also fail
This rules out DNS resolution as the root cause.
Diagnostic Evidence
child_process+curlworks correctlyThis confirms:
Compilation output confirms stdlib is linked
Compare with runtime-only linking (no fetch):
perry devmode also failsThe same
fetchcode fails identically when run viaperry dev, ruling out a compile-only issue.RUST_LOG=debugproduces no additional outputNo networking-related debug messages are emitted, suggesting the error occurs before any network I/O is attempted (possibly in the runtime initialization or FFI bridge).
perry --print-api-manifestconfirms APIs are registeredThe HTTP client modules (
fetch,axios,node:http,net) are also present in the manifest and linked.Comparison with Known Issues
Issue fetch().then() callbacks never fire in macOS native UI apps #6 (closed):
fetch().then()callbacks never fire in macOS native UI apps — root cause wasspawn()infetch.rsnot callingensure_pump_registered(). This was fixed in a later version. Our issue is different: the connection itself fails, not just the callback resolution.Issue The "fetch" example does not seem to work as expected #26 (closed): Fetch example pulled in npm
axiosas JS module — our compilation correctly detects1 native, 0 JavaScript, so this is not the same issue.Workaround
Using
child_process+curlas an HTTP transport proxy:execSync("curl -s ...")— works synchronouslyspawn("curl", ["-s", "-N", "-o", tmpFile, ...])+ file polling withsetInterval+fs.readFileSync— works for SSE streamingThis workaround is functional but has drawbacks:
curlto be available on the target systemMinimal Reproduction
Suggested Investigation Areas
net.connecterror"file name contained an unexpected NUL byte"indicates TypeScript strings are not properly converted to C-style strings (CString) before being passed to socket APIs. This may also affectfetch/axiosinternally when resolving hostnames.node:httprequest()returningundefinedandnet.connectpreviously returning"undefined"error both suggest the FFI error chain is broken — errors from Rust are not properly propagated to TypeScriptEnvironment Exclusion
The following external causes have been ruled out:
socketfilterfw --getglobalstate→ State = 0)codesign -dvvv→flags=0x20002(adhoc,linker-signed), no runtime flag)child_process+curlworks from the same binary, confirming host network is functionalCross-Version Test Results
All versions produce identical error messages, indicating the HTTP client stack has never worked on macOS arm64.