Skip to Content
Examples02 · HTTP Server

02 · HTTP Server

A minimal JSON server built with Node’s http module. Listening on a port needs a network grant.

Source: examples/02-http-server

Run

cd examples/02-http-server 3va run server.js --allow-net=localhost
Server running at http://localhost:3000
curl localhost:3000/ # {"message":"Hello from 3va HTTP server!"} curl localhost:3000/time # {"time":"2026-09-24T23:17:59.074Z"} curl -i localhost:3000/nope # HTTP/1.1 404 Not Found … {"error":"Not found"}
PathResponse
/JSON greeting
/timeCurrent ISO timestamp
anything else404

How it works

http.createServer() and server.listen() are 3va’s Node-compatible implementations. Before binding the socket, 3va checks the network permission. listen() without a host binds to 127.0.0.1 unless --allow-net names 0.0.0.0, :: or *.

HTTPS and WebSocket upgrade

https.createServer({ key, cert }) is a real TLS listener — the key/cert must be PEM and a server created without them fails on listen() rather than serving plaintext. And http servers emit 'upgrade' for Connection: upgrade requests, so ws works end to end:

const https = require("https"); const server = https .createServer({ key, cert }, (req, res) => res.end("ok")) .listen(8443, "127.0.0.1"); server.on("upgrade", (req, socket, head) => { // ws performs the 101 handshake and drives the frames itself });

socket is the raw duplex socket; head holds the bytes read behind the handshake, delivered once. The socket is bridged by a driver task that reads at most 1 MiB ahead of the app and drops a peer that stops reading while more than 32 MiB is queued for it.

Things to keep in mind

  • Without --allow-net the server never starts, and the process still exits with code 0. The permission failure goes to the server’s error event, which this example doesn’t listen for. 3va reports it on exit:

    [!] 1 permission was denied during this run: bind a local server on 0.0.0.0 needs any --allow-net grant (e.g. --allow-net=127.0.0.1)

    The exit code is still 0, so a supervisor or CI job won’t notice. Add a handler so the process fails:

    server.on("error", (err) => { console.error(err.code, err.message); process.exit(1); }); // → EACCES Network access denied. Run with --allow-net=0.0.0.0
  • For listen(), the host in --allow-net limits what binds. Without a host, listen(3000) binds 127.0.0.1. Binding every interface needs a grant naming them: --allow-net=0.0.0.0, :: or * (this is the check_bind rule in the permissions crate). Outbound calls (fetch, http.request, TCP connect) are still matched strictly against the granted host.

  • --allow-net=localhost keeps the server on loopback. Because listen(3000) binds 127.0.0.1 without a wildcard grant, the server accepts connections only on the loopback interface. To serve every interface, grant a wildcard or bind explicitly: server.listen(3000, "0.0.0.0").

  • The port isn’t limited either. --allow-net=localhost:9999 still lets the server listen on 3000.

  • Port 3000 is hard-coded in this example. Reading it from PORT would also need --allow-env=PORT (see 09).