Network transport
Elide ships a host-internal networking layer in dev.elide.lang.javascript.net.transport. Not
exposed to guest JavaScript directly — the edge fetch-handler dispatcher (export default { async fetch(req): Response }), node:net / node:tls /
ws layer on top.
Public types
| Type | Purpose |
|---|---|
HttpClientTransport | Interface — send / sendAsync / push handling. Shared Request / Response / PushHandler records. |
HttpServerTransport | Interface — bind / localPort / close with Handler / Request / Response. |
HttpClient | JDK impl of HttpClientTransport. Singleton via HttpClient.INSTANCE (or HttpClientTransport.shared()). Wraps java.net.http.HttpClient — HTTP/1.1 + HTTP/2 with ALPN, streaming bodies, auto-decompression. |
HttpServer | JDK impl of HttpServerTransport. Singleton via HttpServer.INSTANCE. Built on com.sun.net.httpserver.HttpServer — HTTP/1.1, virtual-thread-per-request executor. |
TcpSocket | Blocking SocketChannel on virtual threads. Half-close semantics. Connect-refused vs reset distinguished by exception type. |
TlsSocket | SSLEngine wrapper with full 5-state handshake loop. Spin guard. ALPN. SNI. Close-drain on shutdown. |
WebSocketClient | Wraps java.net.http.WebSocket. Backpressure-friendly listener. Fragment reassembly. Orderly close with abort fallback. |
WebSocketServer | RFC 6455 implemented directly on TCP — no Netty dependency. Text + binary + ping/pong + close. Mask validation per spec. |
Url | Lightweight URI-based facade. parse, resolve, scheme, host, port, effectivePort, path, query, fragment, pathAndQuery, origin, isSecure, toUri. |
NetException | Runtime exception in Node's shape (code/errno/syscall/operand). |
NetHostInit | Boot-time init; the backend is currently hard-coded to jdk-tls. |
HttpClient.closeShared(), HttpServer.shutdown(), TcpSocket.setKeepAlive /
setNoDelay / peerAddress, and the WebSocket Connection inspectors
(url / subprotocol / isClosed on the client; remoteAddress /
subprotocol / isClosed on the server).
HttpClientTransport / HttpClient
HttpClientTransport is the abstract surface; HttpClient is the JDK-backed implementation
and the sole production instance. Callers route through:
HttpClientTransport.shared()— preferred for new code; returns the singleton via factory.HttpClient.INSTANCE— equivalent direct reference to the impl.HttpClient.sharedJdkClient()— escape hatch returning the underlyingjava.net.http.HttpClient. Used byWebSocketClient(needsnewWebSocketBuilder()); avoid in new code so the abstraction stays meaningful.
The records Request / Response / PushHandler live on HttpClientTransport as shared
nested types — all call sites import them as HttpClientTransport.Request etc. The interface
is @TruffleBoundary-annotated on every method so Truffle PE never inlines the JDK HTTP
machinery.
send is sync; sendAsync returns a CompletableFuture. The shared client uses a
virtual-thread-per-task executor for callbacks (the JDK 21+ recommended pattern).
For Node parity:
Accept-Encoding: gzip, deflateis injected when the caller didn't set their own.Content-Encoding: gzipanddeflateresponses are transparently decompressed before the body InputStream is returned.- Body reads stream —
Response.bodyis a liveInputStream; close to release the connection.Response.bodyBytes()is a small-body helper that closes for you.
Caveats inherited from the JDK:
- The JDK has no body-read timeout —
HttpRequest.timeout()covers headers + first byte of body only. For streaming consumers that need a body deadline, layer your own. - Pool tunables (
jdk.httpclient.connectionPoolSize,keepalive.timeout) are JVM-wide system properties; you cannot tune one client differently from another in the same process. - HTTP/2 server push (
PushPromiseHandler) is supported by the JDK but Chrome removed support in 2022 — accepted but not actively used.
HttpServerTransport / HttpServer
HttpServerTransport is the abstract surface; HttpServer is the JDK-backed implementation
built on com.sun.net.httpserver.HttpServer. Access via:
HttpServerTransport.shared()— preferred; returns the singleton via factory.HttpServer.INSTANCE— equivalent direct reference.
The records Request and the Handler functional interface live on HttpServerTransport as
shared nested types. Response is an interface — HttpServer.JdkResponse is the production
impl; mock impls in tests must mirror the exactly-once send / sendText / sendStreaming
guard the interface contract spells out.
bind returns an opaque long handle — implementation-private (do not pass between
transports). All methods are @TruffleBoundary-annotated. The server uses a
virtual-thread-per-request executor — blocking IO inside a handler costs only a parked
vthread, not a platform thread.
HTTP/2 server, server push, native transports, and zero-copy file serving via
DefaultFileRegion are deferred to a future Netty-backed HttpServerNetty (Netty is on the
runtime's transitive classpath via Ktor but not enabled for the base package by default —
wiring it requires a separate native-image substitution pass).
TcpSocket
Blocking SocketChannel on virtual threads. JDK 21+ guidance: prefer this over
AsynchronousSocketChannel — Loom inverts the calculus. The selector-based path remains
correct but is no longer the recommended default for a per-socket abstraction.
Half-close semantics map to Node:
socket.end()→shutdownOutput. Flushes pending writes, sends FIN. Peer reads EOF; we can still read.socket.destroy()→close(handle, abort=true). SetsSO_LINGER=0so close sends RST. Use only on error paths; harms the peer's keep-alive bookkeeping.
Connection-refused vs connection-reset is distinguished by JDK exception type:
ConnectException → ECONNREFUSED; SocketException with "reset" message → ECONNRESET;
"broken pipe" → EPIPE. FIN on an established connection is not an exception — read returns
-1.
TlsSocket
Full SSLEngine state-machine wrap.
- Handshake loop:
NEED_WRAP→wrap+ write;NEED_UNWRAP→ read +unwrap;NEED_TASK→ drain delegated tasks;NOT_HANDSHAKING/FINISHEDexits. - Spin guard: two consecutive zero-progress iterations (no bytes consumed or produced) fail with
EIO— guards against the canonical Apache MINA / Netty #5975 hang. - Buffer asymmetry:
BUFFER_UNDERFLOW→ read more from the socket (don't grow the destination);BUFFER_OVERFLOW→ drain or grow the destination (don't read more). - ALPN:
setApplicationProtocolsis set beforebeginHandshake; post-handshake protocol is read viagetApplicationProtocol. - SNI: explicit
setServerNamesfor hostname targets (the JDK only auto-fills SNI when the engine is created with the hostname constructor, and only when the host is not an IP literal). - Close drain: send
close_notifyviawrap, drain outbound untilisOutboundDone, unwrap inbound untilisInboundDoneso the peer's reply is consumed. Skipping the inbound drain is the canonical "truncation attack" warning.
WebSocketClient
Wraps java.net.http.WebSocket. The internal Listener translates the JDK's
"return-CompletionStage-for-backpressure" contract into a higher-level Handler with reassembly
of fragmented messages (the JDK delivers continuation frames as multiple onText(ws, text,
last=false) calls; the ws package surfaces one 'message' per logical frame).
Auto-pong: the JDK auto-replies to peer Ping with Pong — there is no API to disable this.
Handler.onPing / onPong are informational.
Connection.close(code, reason) sends a Close frame and arms a watchdog: if the peer doesn't
reply within 5 seconds, the underlying TCP is aborted via WebSocket.abort(). This matches the
ws package's WebSocket.close(code, reason) semantics.
Maximum message size: 64 MiB by default. Overflow forces the connection closed with a
synthesised EOPNOTSUPP error — the standard POSIX table doesn't carry EMSGSIZE so we use the
closest documented match.
WebSocketServer
RFC 6455 implemented directly on top of java.net.ServerSocket — no Netty dependency. Covers
the surface that ws's server side uses:
- Handshake: HTTP/1.1 GET with
Upgrade: websocket,Connection: Upgrade,Sec-WebSocket-Version: 13, non-emptySec-WebSocket-Key. Replies with101 Switching Protocolscarrying theSec-WebSocket-Accept(SHA-1 ofKey + RFC6455-magic-GUID, base64-encoded). - Frames: text, binary, ping (auto-pong), pong, close. Continuation frames reassembled before the handler is invoked.
- Mask validation: per RFC 6455 §5.1, client-to-server frames must be masked. We refuse unmasked frames with a
1002(Protocol Error) close. - Maximum message size: 64 MiB; overflow forces a
1009(Message Too Big) close.
Compression (permessage-deflate) is an enhancement deferred to a follow-up. Sub-protocol
negotiation is implemented on both the client and the server.
Errno surface
NetException carries a Node-style POSIX code, the platform errno resolved via
FsErrno.numberFor, the syscall name, and a primary operand (host:port, URL, signal name,
etc.). FsErrno.fromException classifies the standard JDK network exceptions:
| JDK exception | code |
|---|---|
java.net.ConnectException | ECONNREFUSED |
java.net.NoRouteToHostException | EHOSTUNREACH |
java.net.PortUnreachableException | ECONNREFUSED |
java.net.BindException | EADDRINUSE |
java.net.SocketTimeoutException | ETIMEDOUT |
java.net.http.HttpTimeoutException | ETIMEDOUT |
java.net.UnknownHostException | EHOSTUNREACH |
java.net.SocketException w/ "reset" | ECONNRESET |
java.net.SocketException w/ "broken pipe" | EPIPE |
java.net.SocketException w/ "address already in use" | EADDRINUSE |
java.net.SocketException w/ "address not available" | EADDRNOTAVAIL |
java.net.SocketException w/ "not connected" | ENOTCONN |
FsErrno's table was extended with the standard network codes: ECONNABORTED, EAFNOSUPPORT,
EADDRNOTAVAIL, ENETDOWN, ENETUNREACH, EHOSTUNREACH, EINPROGRESS, ENOBUFS, EISCONN,
ENOTCONN, ENOTSOCK, EOPNOTSUPP. Linux + macOS + MSVCRT errno values are populated.
Threading
The HTTP client (HttpClient.INSTANCE) is a per-process singleton with a virtual-thread
executor. The HTTP server uses a virtual-thread-per-request executor. The TCP/TLS surfaces are
blocking on virtual threads — Loom-friendly. The WebSocket server runs one accept thread plus
one handler vthread per connection.
Naming
The package is net.transport rather than the *.host suffix used by fs.host and
process.host because transport is not a reserved Java keyword (the host suffix on the
other layers escapes the native reserved word). Functionally the layer is host-internal —
guest JS reaches it only through the node:* modules layered on top.