This reference describes the implemented native runtime. Historical paths and internal version labels are retained where relevant. Later extension chapters supersede older limitations.
Stage 2 adds an original RFC6455 codec on the existing TCP/verified-TLS transport.
It does not implement stage 3 (webhook/MQTT/IRC/framing), compression, HTTP/2,
proxying, redirects, or a production long-lived service. The pre-code finite design
is WEBSOCKETS-DESIGN.md. Parent specification and quality reviews remain required.
Originals and every baseline, including baselines/pre-websockets, are immutable.
API
websocket client $result $connection $config timeout_ms
websocket server $result $connection $config timeout_ms
websocket step $result $data $connection
websocket send $result $connection text|binary|ping data
websocket close $result $connection code reason timeout_ms
websocket abort $connection
All destinations must be declared. Metadata becomes typed JSON. $data is a
binary-safe scalar, including embedded NUL. Step destinations must differ. Metadata
may already hold JSON but not a legacy array; data may not hold either. Handle,
argument, destination and conservative result-storage checks precede I/O. A
preflight error preserves output sentinels and a usable handle. Protocol or transport
failure invalidates all copied aliases; socket wait also invalidates expired
selected handles. As in Stage 1, fatal resource failures cannot be caught.
client / server claim an idle established stream in place, retaining its
handle and aliases. No unread legacy buffer, pending TLS read/write, transport EOF,
or incomplete TLS handshake is permitted. socket connect / tlsconnect still
have their existing bounded but blocking setup semantics; do not use those setup
commands inside a latency-sensitive event loop. The HTTP upgrade itself is fully
staged. TLS servers use tlslisten, tryaccept, and handshake first. There is no
new insecure TLS option, and client chain/hostname verification is unchanged.
$config is a JSON object with exactly four string fields:
{"host":"127.0.0.1:12345","path":"/ws","origin":"http://127.0.0.1:23456","protocol":"vanilla.dashboard"}
host is the exact HTTP Host value, including any explicit port; it is not a DNS
lookup override. path is an absolute request target beginning / with no fragment.
Whitespace/control characters are rejected in all four fields, and each is limited
to 1024 bytes. Host cannot contain /, backslash, @, ?, or #. Protocol is empty
or one HTTP token. This is intentionally not a URL parser or multi-protocol chooser.
Server path/Host/Origin must match exactly. Empty configured Origin requires an
absent Origin header, not a wildcard. Browser deployments must configure their
specific HTTP page origin. Non-browser clients can forge Origin; it is not identity
or authentication. A configured subprotocol must be offered/selected exactly;
empty means no selected protocol. Before sending 101, the server validates extension
offers using RFC6455 §9.1: extension and parameter names are tokens; parameter
values are tokens or quoted strings whose unescaped value must itself be a nonempty
token. Quoted-pair escapes are processed, not blindly accepted. Valid unsupported
offers (including browser permessage-deflate defaults) are declined without any
extension selection; malformed offers fail the connection. Client rejects any
extension selection. This is syntax validation, not extension-specific semantics.
Both roles require --allow-network; server upgrade additionally requires
--allow-listen. TLS server credentials remain explicit operator CLI
--tls-cert-file PATH --tls-key-file PATH, validated before binding. A private test
CA can be supplied to a client with --tls-ca-file PATH.
Progress and accounting
Every result has status, want, accepted, count, completed, pending,
type, and code fields:
client/server:handshake; no network I/O. Client queues its HTTP request.send:progress, payload accepted into owned bounded storage; no network I/O. One outstanding text/binary message per handle; another is rejected, never implicitly retried. Ping is limited to 125 bytes and shares the bounded queue.step:handshake,open,progress,wouldblock,message,ping,pong,closing, orclosed. At most one transport read or write per call, and at most one complete HTTP header/frame processed. Incomplete parse work is bounded. Step can complete output before reading; continue the event loop.accepted: new payload bytes copied by this call. For close this includes the two-byte code and reason; step is zero. Zero-length messages still enqueue a frame.count: wire bytes newly written by this step, including masks/headers/control bytes. It is not application data received or proof of peer delivery.completed: application payload bytes whose entire outgoing text/binary frame drained in this step. Zero for controls/HTTP and for partial writes.pendingtells whether any queued/current output remains; zero-byte-message completion is observable via pending becoming false, not a positive completed count.message:$datais a complete message;typeistextorbinary. Receive size is the byte length of$data, notcount.ping/pongreturn their payload. Ping automatically queues an identical pong.closingcarries the received close reason (without code);codeis received code, 1005 for an empty close, or zero before a close is received. 1005 is never transmitted.wantexposes current physical retry direction (rorw);closeduses empty. Do not translate it yourself into a raw transport operation.
Use logical r for WebSocket handles in socket wait, even while writing:
set $interests "[{\"handle\":\"" $h "\",\"events\":\"r\"}]"
json parse $interests $interests
socket wait $events $interests 20
websocket step $r $data $h
Wait maps that interest to queued output, transport/TLS WANT, buffered parse work,
or the next deadline. It performs no protocol I/O. For a WebSocket, a true read
readiness flag means retry step, even if the physical operation is a write. The
legacy wait handshake boolean still describes TLS transport handshaking;
WebSocket upgrade phase is reported by the WebSocket result's status.
Existing plain/UDP/TLS interests
retain their Stage 1 meanings and can share the same interest list. Expiry is an
error/expired wait event, not clean EOF; read/write readiness never guarantees
progress. Raw socket recv/readline/sendall/tryrecv/trysend/handshake cannot bypass a
claimed WebSocket. socket endpoint, socket close and wait remain permitted.
abort / raw close discard protocol output; graceful shutdown requires close/step.
close validates a code/reason and queues the close behind already accepted output.
Reason is UTF-8, at most 123 bytes. Legal transmitted codes are 1000–1014 except
1004/1005/1006, and application/private 3000–4999. It starts its absolute deadline,
1–30000 ms. Valid already-in-flight incoming messages during local closing are
validated then discarded, never delivered as new application actions. The peer's
close is echoed if necessary; closed means both frames were exchanged and the
handle was released. EOF without that exchange is a truncation error, including
EOF at a frame boundary. TLS truncation remains independently detected by OpenSSL.
Protocol and resource boundaries
- HTTP/1.1 GET upgrade only, 8192 header bytes / 64 fields. Strict CRLF, no folding,
ambiguous singleton duplicates, body lengths or transfer encoding. Upgrade and
Connection are case-insensitive comma-token lists; version is exactly 13. Keys
must be canonical base64 of exactly 16 bytes. Client verifies SHA1/base64 accept.
Trailing bytes after the header delimiter remain available for frame processing.
Extension-offer scanning is linear and constant-storage within that same header
byte bound. RFC2616
1#empty comma-list elements are allowed only if at least one actual extension is present. Existing stricter no-folding and duplicate-field rejection remains unchanged, including duplicate extension header fields. - Client masks use OpenSSL
RAND_bytes; server sends unmasked. Wrong masking, any RSV bit, unknown opcode, fragmented/oversized controls, noncanonical lengths, the 64-bit high bit, frame overflow and invalid fragmentation sequencing fail. - Frame and reassembled message payload maximum: 65536 bytes. Text UTF-8 is validated incrementally as each complete frame is processed, retaining constant continuation state across fragments and interleaved controls. Impossible prefixes fail immediately (including overlong, surrogate and above-Unicode prefixes); valid incomplete codepoints may cross fragment boundaries but must be complete at message FIN. The same validation runs before local-close discard. A peer Close may interrupt a still-valid incomplete message; it cannot hide an invalid prefix already processed. Partial wire frames remain buffered until complete. No compression or extension semantics. Outgoing messages use one FIN frame.
- One application output plus a bounded queue of at most eight frame/HTTP entries; the currently written frame is separate. The eight-entry limit includes queued application data, ping/pong and close. A new application message is rejected until its predecessor drains. Queue overflow while handling a peer is a protocol error closing that peer, not unbounded memory growth. Input is capped at 73742 bytes (one maximum masked frame plus one transport read); reassembly is separately capped.
- Existing 32 handles, 4096 network calls, 16 MiB wire send/receive, 1 MiB aggregate socket/protocol retained storage, 60000 ms cumulative timeout reservations remain shared with TCP/UDP. Capacity and temporary-copy headroom are charged conservatively. Creation reserves its upgrade timeout plus 1000 ms for a potential peer-initiated close. Explicit local close reserves its own timeout. Fatal resource limits do not promise completing a close handshake.
- Stable owned TLS retry buffers survive WANT. Write retries cannot move storage or change arguments. Read/WANT_READ allows compatible writes while preserving its 8192-byte read buffer; read/WANT_WRITE remains exclusive. No internal retry loop, sleeping helper thread, or hidden blocking readline is used by this protocol. DNS/connect, local certificate reads, crypto/library allocation and OS scheduling retain Stage 1 cooperative rather than hard-real-time limitations.
Real examples and browser verification
examples/websocket_dashboard.v is the actual multi-peer backend. It owns the
counter, echo and revisions, validates incoming JSON operations server-side, and
broadcasts snapshots. Its bounded application policy coalesces latest snapshots
while a slow peer has an accepted output pending: it never blindly resends an
accepted action or grows an unlimited per-peer queue. It accepts at most eight
peers, binds loopback, expires incomplete peers, and runs at most 20 seconds / 1000
loop turns under unchanged runtime budgets. It is a finite demonstration, not a
service daemon. Invalid application controls disconnect only their sender.
examples/dashboard/{index.html,dashboard.css,dashboard.js} are local static assets,
with increment/reset/echo and an explicit reconnect button. There are no CDNs,
fake state mutations, optimistic counter increments, or action retries. Page reload
and reconnect read the current Vanilla state. The wrapper's Python HTTP server
serves these files only; it does not handle WebSockets or own dashboard state.
Run the complete two-browser fixture (plain plus TLS):
cd /root/vanilla1
build/ws-venv/bin/python tests/run_websocket_dashboard.py ./build/vanilla1
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 UBSAN_OPTIONS=halt_on_error=1 build/ws-venv/bin/python tests/run_websocket_dashboard.py ./build/vanilla1-asan
It chooses ephemeral loopback ports, passes the exact HTTP Origin to Vanilla, records real backend state logs, checks both browsers and JavaScript errors, performs reload/reconnect without replaying actions, isolates a still-live stalled peer during a healthy action, exercises invalid controls, and observes zero clients after disconnect. Every subprocess/socket/thread is bounded and torn down. After asserting zero open clients, the browser fixture terminates/reaps the remaining finite backend instead of waiting for its 20-second idle lifetime; ASan/UBSan run throughout the exercise, but exit-time LeakSanitizer does not run for that SIGTERM teardown. The normal-exit protocol/client fixtures do run with leak detection. TLS browser testing uses a freshly generated local CA/leaf plus only that leaf's SPKI exception in isolated Chromium. This is a browser fixture exception, not an interpreter verification bypass. Trusted Python TLS peers verify the CA normally.
Screenshots: docs/websocket-dashboard-{release,asan}-{plain,tls}.png. Machine-readable
browser results and actual Vanilla snapshots use the corresponding JSON names.
examples/websocket_client.v runs against the trusted Python WebSocket server in
tests/test_websocket_limits.py, plain and verified TLS, and prints exactly:
echo: hello from Vanilla
WebSocket close exchange complete
Dependencies and rerun commands
The interpreter adds no new library: original codec plus already linked OpenSSL.
Primary references are RFC6455 sections 4–9 and OpenSSL 3 RAND_bytes, SSL_read,
SSL_write, SSL_get_error / existing Stage 1 ownership documentation. Retrieved RFC
and RAND_bytes source pages are build/rfc6455.txt and build/RAND_bytes.html.
The extraction tool lacked a fetch backend; direct HTTPS retrieval succeeded.
Test-only local venv: websockets==16.0 (BSD-3-Clause) and playwright==1.58.0
(Apache-2.0), with existing cached Chromium 1223. Installed distribution version,
license expression and license paths are in docs/websockets-dependencies.json.
No third-party WebSocket source was copied into the interpreter. For a fresh setup:
python3 -m venv build/ws-venv
build/ws-venv/bin/pip install websockets==16.0 playwright==1.58.0
# If no Chromium is already installed, install this test-only browser:
build/ws-venv/bin/playwright install chromium
make release sanitize
build/ws-venv/bin/python tests/test_websockets.py ./build/vanilla1
build/ws-venv/bin/python tests/test_websocket_limits.py ./build/vanilla1
python3 tests/verify_websockets.py
make test and make test-sanitize include the new protocol/limits/browser suites
and require that venv. The complete verifier additionally repeats all seven cache
baseline differentials and records exact counts, commands, logs and binary hashes.
Final verified evidence / limitations
3304/3304 checks per artifact passed on the final warning-clean release and
ASan/UBSan builds, with leak detection and halt-on-error. This is the prior 2764,
plus another 339-case pre-websockets differential, 182 WebSocket protocol cases,
17 ownership/limits/client-demo cases and two browser scenarios. The 339-case corpus
is repeated against seven baselines; these are checks, not 3304 unique programs.
The protocol count preserves all prior 59 cases and adds 122 SPEC regressions plus
one permanent TLS MemoryBIO retry/readiness case. Before source edits, the 122 new
SPEC cases passed 30 and failed 92 on each original artifact; after correction,
122/122 passed on each. Red/green logs and JSON are in build/ws-spec-fix/.
Outside those totals, the old pre-network-suite binary passed 73 TCP and 70 DNS/TLS
checks, and pre-websockets passed all 79 foundation checks. All 19 original and
baseline files retain their captured SHA-256 hashes. No fixture interpreter or
cached Chromium process remained after verification (docs/websockets-cleanup.json).
All 65 captured original/baseline/independent-review artifact hashes also match;
build/ws-spec-fix/final-audit.json checks exact final binary hashes against the
verifier and unchanged transport includes. Prior final reports/logs and the old
binaries were preserved under build/ws-spec-fix/before/ before replacements.
Full command: python3 tests/verify_websockets.py; complete transcript:
build/websockets-full-verifier.log. Exact per-command counts/durations, binary
hashes and log paths: docs/websockets-verification.json. Preservation:
docs/websockets-preservation.json. Protocol/limits results:
docs/websockets-tests*.json and docs/websockets-extra*.json. Browser output is
PASS dashboard TLS=False, PASS dashboard TLS=True, with 2/2 scenarios per
artifact, zero JavaScript errors and zero final connected clients. The synchronized
plain and TLS pressure fixtures each verify 4,192,182 application bytes in 64
individually tagged messages, only begin draining after observed wouldblock, and
match whole-message completion accounting exactly.
Earlier failures remain available, not suppressed: build/websockets-build-initial.log
(two misleading-indentation warnings subsequently corrected),
build/websockets-browser-initial.log (container renderer crash and fixture certificate
directory setup failure), build/websockets-extra-initial.log (a peer incorrectly
expected request bytes after a rejected preflight), and
build/websockets-close-race-red-asan.log. The latter exposed the original rejection
of valid data already in flight during local closing. It is now validated/discarded
while the close exchange completes; the final clean build passes that regression
on both artifacts. The incomplete first verifier transcript and JSON are retained
as build/websockets-full-verifier-first.log and
build/websockets-initial-verification.json; they are not the final evidence.
The SPEC remediation's first two full-verifier attempts encountered intermittent
TLS dashboard upgrade expiry on a Python malformed-control peer. They are preserved
under build/ws-spec-fix/full-first/ and full-second/; the third complete run
passed unchanged suites on both builds. Diagnostic evidence shows expiry rather
than extension-syntax rejection, but the timing failure's root cause remains
unresolved. No tests, deadlines or TLS checks were relaxed. Full details, including
the unsuccessful optional strace diagnostic, are in docs/websockets-spec-fixes.md.
This passing implementation run is not independent SPEC or quality approval.
The suite contains raw-frame HTTP/protocol adversaries, plain/TLS boundary sizes, trusted-library interop in both directions, fragmentation/coalescing/UTF-8/control/close cases, wrong masks/lengths, slow handshake and closing deadlines, injection rejection, output sentinel and raw-bypass guards, alias invalidation, shared fatal budgets, and synchronized backpressure with exact per-block-tagged bytes and completion accounting. All prior core/utilities/worklog/TCP/DNS/TLS/foundation tests remain.
Not claimed: Autobahn certification, broad fuzzing, deterministic allocator/OS EINTR injection, a WebSocket-specific forced TLS key-update read/WANT_WRITE or write/WANT_READ fixture, whole-process RSS limits, mutual TLS, authentication, production persistence/rate policy, hard-real-time crypto, compression, outbound fragmentation, HTTP fallback/error pages, or unlimited server lifetime. Existing Stage 1 MemoryBIO coverage remains in the full prior suite. Closing/protocol errors may discard unsent data; applications must not interpret acceptance as delivery. Both confirmed SPEC fixes are implemented; independent SPEC re-review remains pending, followed by quality review. Stage 3 has not been started.