Sockets

runtime:net is raw TCP following the WinterTC Sockets API: connect() for outbound connections, listen() for a server. Bytes move over web streams — nothing blocks. bind() is UDP, and has its own guide.

Client

Opening an outbound connection needs the Net capability (the esrun CLI grants it). Closing the writable half-closes (sends FIN) while reads continue; pass allowHalfOpen: true to keep writing after the peer's FIN.

JavaScript
import { connect } from "runtime:net";

// connect() returns a Socket synchronously; .opened settles once connected.
const sock = connect({ hostname: "example.com", port: 80 });
await sock.opened;

const w = sock.writable.getWriter();
await w.write(new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n"));
await w.close();                           // half-close: send FIN, keep reading

// Decode through TextDecoderStream so a multi-byte character split across two
// chunks is still decoded correctly.
let body = "";
for await (const chunk of sock.readable.pipeThrough(new TextDecoderStream())) {
  body += chunk;
}

TLS

secureTransport: "on" negotiates TLS with certificate verification on. sni sets the server name (used for both the SNI extension and hostname verification), and alpn offers protocols — the negotiated one comes back as opened.alpn.

JavaScript
import { connect } from "runtime:net";

// secureTransport: "on" — TLS with certificate verification, offering ALPN.
const sock = connect({ hostname: "example.com", port: 443 }, {
  secureTransport: "on",
  sni: "example.com",                      // optional; defaults to the host
  alpn: ["h2", "http/1.1"],
});
const { alpn } = await sock.opened;        // negotiated protocol, e.g. "h2" (or null)

STARTTLS

secureTransport: "starttls" opens plaintext and upgrades the same connection in place via startTls(), which returns a new encrypted Socket (the original is consumed).

JavaScript
import { connect } from "runtime:net";

// "starttls" opens plaintext, then upgrades the SAME connection in place
// (the SMTP/IMAP/XMPP pattern).
const sock = connect({ hostname: "mail.example.com", port: 143 }, {
  secureTransport: "starttls",
});
// ... exchange the plaintext go-ahead, then:
const tls = sock.startTls();               // a new, encrypted Socket
console.log(tls.upgraded);                 // true

Server

listen() binds a server (needs NetListen) and yields each accepted Socket as an async iterable. port: 0 picks an ephemeral port — read it from addr.

JavaScript
import { listen } from "runtime:net";

const server = listen({ hostname: "127.0.0.1", port: 8080 });
const { port } = await server.addr;        // resolves once listening

for await (const conn of server) {         // each accepted Socket, already open
  conn.readable.pipeTo(conn.writable);     // echo
}

TLS termination

secureTransport: "on" on the server terminates TLS on every accept — pass a PEM cert + key (and optional alpn). The cert/key are supplied inline, so server TLS needs no capability beyond the NetListen the bind already requires.

JavaScript
import { listen } from "runtime:net";
import { file } from "runtime:fs";

// Terminate TLS on accept. cert/key are inline PEM (string or bytes), so the
// guest loads them itself — server TLS needs no capability beyond NetListen.
const server = listen({
  hostname: "127.0.0.1", port: 8443,
  secureTransport: "on",
  cert: await file("cert.pem").text(),     // PEM chain, leaf first
  key: await file("key.pem").text(),       // PKCS#8 / PKCS#1 / SEC1
  alpn: ["h2", "http/1.1"],
});

for await (const conn of server) {
  const { alpn } = await conn.opened;      // negotiated protocol
  conn.readable.pipeTo(conn.writable);     // every byte is encrypted
}

Unix domain sockets

A local service that listens on a path — the Docker Engine API, PostgreSQL or Redis on the same host, a sidecar — is reached with path in place of the host and port. The rest of the Socket is the same.

JavaScript
import { connect } from "runtime:net";

const docker = connect({ path: "/var/run/docker.sock" });
const writer = docker.writable.getWriter();
await writer.write(new TextEncoder().encode(
  "GET /version HTTP/1.1\r\nHost: docker\r\nConnection: close\r\n\r\n",
));
Shell
esrun --allow-net=unix:/var/run/docker.sock app.js

A server listens on a path the same way:

JavaScript
import { listen } from "runtime:net";

const server = listen({ path: "/run/app/app.sock" });
for await (const conn of server) conn.readable.pipeTo(conn.writable);
Shell
esrun --allow-listen=unix:/run/app/app.sock server.js

The path must be absolute, and it must be named in the flag: a bare --allow-net reaches the network, not the sockets on the machine. Closing the listener removes its socket file. A file left by a server that was killed makes the next listen fail with ERR_ADDRESS_IN_USE; remove it before starting again. Unix domain sockets are not available on Windows.

UDP

bind() is a datagram socket: send/receive instead of streams, and two capabilities instead of one. It has its own guide.

See also

Last updated on
Edit this page