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

TypePurpose
HttpClientTransportInterface — send / sendAsync / push handling. Shared Request / Response / PushHandler records.
HttpServerTransportInterface — bind / localPort / close with Handler / Request / Response.
HttpClientJDK 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.
HttpServerJDK impl of HttpServerTransport. Singleton via HttpServer.INSTANCE. Built on com.sun.net.httpserver.HttpServer — HTTP/1.1, virtual-thread-per-request executor.
TcpSocketBlocking SocketChannel on virtual threads. Half-close semantics. Connect-refused vs reset distinguished by exception type.
TlsSocketSSLEngine wrapper with full 5-state handshake loop. Spin guard. ALPN. SNI. Close-drain on shutdown.
WebSocketClientWraps java.net.http.WebSocket. Backpressure-friendly listener. Fragment reassembly. Orderly close with abort fallback.
WebSocketServerRFC 6455 implemented directly on TCP — no Netty dependency. Text + binary + ping/pong + close. Mask validation per spec.
UrlLightweight URI-based facade. parse, resolve, scheme, host, port, effectivePort, path, query, fragment, pathAndQuery, origin, isSecure, toUri.
NetExceptionRuntime exception in Node's shape (code/errno/syscall/operand).
NetHostInitBoot-time init; the backend is currently hard-coded to jdk-tls.
Lifecycle and inspector methods across these types include: 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 underlying java.net.http.HttpClient. Used by WebSocketClient (needs newWebSocketBuilder()); 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, deflate is injected when the caller didn't set their own.
  • Content-Encoding: gzip and deflate responses are transparently decompressed before the body InputStream is returned.
  • Body reads stream — Response.body is a live InputStream; 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 timeoutHttpRequest.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). Sets SO_LINGER=0 so 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: ConnectExceptionECONNREFUSED; 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_WRAPwrap + write; NEED_UNWRAP → read + unwrap; NEED_TASK → drain delegated tasks; NOT_HANDSHAKING / FINISHED exits.
  • 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: setApplicationProtocols is set before beginHandshake; post-handshake protocol is read via getApplicationProtocol.
  • SNI: explicit setServerNames for 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_notify via wrap, drain outbound until isOutboundDone, unwrap inbound until isInboundDone so 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-empty Sec-WebSocket-Key. Replies with 101 Switching Protocols carrying the Sec-WebSocket-Accept (SHA-1 of Key + 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 exceptioncode
java.net.ConnectExceptionECONNREFUSED
java.net.NoRouteToHostExceptionEHOSTUNREACH
java.net.PortUnreachableExceptionECONNREFUSED
java.net.BindExceptionEADDRINUSE
java.net.SocketTimeoutExceptionETIMEDOUT
java.net.http.HttpTimeoutExceptionETIMEDOUT
java.net.UnknownHostExceptionEHOSTUNREACH
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.