o
odin.langpkg.dev
packages / library / netclients

netclients

351f333library

A network-client library for Odin

No license · updated 2 weeks ago

netclients

A network-client library for Odin, built on one shared sans-I/O engine:

  • HTTP/1.1 + HTTPS client — methods, chunked & content-length & close-delimited bodies, keep-alive connection pooling, redirects, transparent gzip/deflate, Basic/Bearer auth, a cookie jar, and TLS (SNI/ALPN, verify-on-by-default, custom CA).
  • WebSocket client — the full RFC 6455 client protocol: masking, all frame length forms, fragmentation with interleaved control frames, auto-pong, the close handshake with the full close-code set, and incremental UTF-8 validation.
  • JSON-RPC 2.0 client — over HTTP (the Bitcoin Core / Drivechain RPC path) or WebSocket (long-lived, multiplexed, with server-push notifications). Typed and dynamic calls, batches, and a strict three-tier error model.
  • Server-Sent Events (SSE) client — a streaming text/event-stream reader that delivers events as they arrive (not a buffered response), with auto-reconnect (Last-Event-ID + the server's retry:). Sync, async (core:nbio), and threaded.
  • ZeroMQ SUB client — a ZMTP 3.1 subscriber over tcp:// (NULL security), for Bitcoin Core ZMQ notifications. Multipart messages [topic, body, sequence], PING→PONG keepalive, subscribe/unsubscribe. (A focused subset, not all of ZeroMQ — see BITCOIN.md for the planned bitcoin package that pairs it with Core RPC.)

Every client runs synchronously, asynchronously (core:nbio), or on a thread pool (core:thread) over the same protocol code.

Status: pre-1.0. Implemented and tested: sync + async (core:nbio) + threaded HTTP/S with pooling, the full RFC 6455 WebSocket client (sync and async-over-TLS), JSON-RPC 2.0 over HTTP and WebSocket, and the HTTP feature set (redirects, cookies, compression, auth, body encoders). HTTP/2: the HPACK (RFC 7541) codec is implemented and vector-tested; framing/multiplexing is the remaining v0.6 work. Async TLS is implemented across HTTP/RPC/WebSocket/SSE. permessage-deflate and a few HTTP edge features are still future work. See CLAUDE.md for the authoritative status.


Requirements

Requirement Notes
Odin compiler dev-2026-06 or newer (uses the redesigned core:os, [dynamic; N]T, #+feature/#+build).
OpenSSL 3.x Required for TLS (https://, wss://) and the WebSocket accept hash (SHA-1). Linked at build time; must be installed on the system (by design).
OS macOS, Linux, and Windows are all supported.

The sans-I/O protocol cores have no third-party dependencies — only core: and base: packages. OpenSSL is the single external native dependency.


Installing the requirements

Odin

Install the Odin compiler (dev-2026-06+) and put odin on your PATH. See https://odin-lang.org/docs/install/. Verify with:

odin version    # expect dev-2026-06 or newer

OpenSSL 3.x — per OS

macOS (Homebrew):

brew install openssl@3

openssl@3 is keg-only, so this library adds the Homebrew lib directory to the linker search path automatically:

  • Apple Silicon: /opt/homebrew/opt/openssl@3/lib
  • Intel: /usr/local/opt/openssl@3/lib

No further configuration is needed.

Linux (system OpenSSL + headers/dev package):

# Debian / Ubuntu
sudo apt-get install libssl-dev

# Fedora / RHEL
sudo dnf install openssl-devel

# Arch
sudo pacman -S openssl

The library links system:ssl and system:crypto.

Windows (OpenSSL 3.x):

Install an OpenSSL 3.x distribution (e.g. the Shining Light "Win64 OpenSSL" installer, or vcpkg install openssl). Then:

  1. Make the import libraries (libssl.lib, libcrypto.lib) available to the linker (add their directory to LIB, or copy them into your project).
  2. Ship the runtime DLLs (libssl-3-x64.dll, libcrypto-3-x64.dll) on the PATH next to your executable.

If OpenSSL is missing, the build fails at link time with a clear unresolved-symbol error for the libssl/libcrypto imports.


Quick start

package main

import nc "netclients"   // adjust to your import path
import "core:fmt"

main :: proc() {
	// --- HTTP GET (sync) ---
	resp, err := nc.http_get("https://example.com/")
	if err != nil {
		fmt.eprintfln("error: %v", err)
		return
	}
	defer nc.response_destroy(&resp)
	fmt.printfln("%d %s", resp.status.code, nc.response_body_string(&resp))
}

A reusable client (pooling, redirects, cookies, TLS options)

jar: nc.Cookie_Jar
nc.jar_init(&jar)
defer nc.jar_destroy(&jar)

opts := nc.default_client_options()
opts.cookie_jar = &jar          // store/replay cookies
// opts.insecure_skip_verify = true   // explicit, named, loud — off by default
c := nc.client_make(opts)
defer nc.client_destroy(&c)     // closes pooled idle connections

req: nc.Request
nc.request_init(&req, .Post, "https://api.example.com/things", context.allocator)
defer nc.request_destroy(&req)
nc.headers_set(&req.headers, "Authorization", nc.bearer_auth_header("token"))
req.body = nc.form_urlencode({{"name", "ada"}, {"role", "admin"}})

resp := nc.client_do(&c, &req) or_else nc.Response{}
defer nc.response_destroy(&resp)

JSON-RPC over HTTP (Bitcoin Core flavor)

rpc, _ := nc.rpc_http_make("http://user:pass@127.0.0.1:8332/")  // Basic auth from URL
defer nc.rpc_destroy(&rpc)

count := nc.rpc_call_typed(&rpc, "getblockcount", nil, int) or_return
hash  := nc.rpc_call_typed(&rpc, "getblockhash", []int{count}, string) or_return
defer delete(hash)

A server-returned error surfaces as an Rpc_Fault (distinct from transport and protocol errors) — match it for code/message/data.

WebSocket

ws := nc.ws_dial("wss://echo.example.com/") or_return
defer nc.ws_destroy(&ws)

nc.ws_send_text(&ws, "hello") or_return
msg := nc.ws_recv(&ws) or_return
defer nc.ws_message_destroy(&msg)
nc.ws_close(&ws, 1000, "bye")

Server-Sent Events (SSE)

conn := nc.sse_connect("https://stream.example.com/events") or_return  // auto-reconnects
defer nc.sse_destroy(&conn)

for {
    ev, err := nc.sse_recv(&conn)      // blocks until the next event
    if err != nil { break }            // stream ended (with auto_reconnect off)
    defer nc.sse_event_destroy(&ev)
    fmt.printfln("[%s] %s", ev.event, ev.data)   // ev.event / ev.data / ev.id
}

// Async (core:nbio): callbacks on the event loop — see examples/sse_listen_async.
conn := nc.async_sse_connect(url, on_open, on_event, on_error, &state) or_return
defer nc.async_sse_destroy(conn)
nbio.run()

// Threaded: stream on a background worker, callbacks fire there — see examples/sse_listen_threaded.
stream := nc.sse_stream_start(url, on_event, on_error, &state)
// ... main thread stays free ...
nc.sse_stream_stop(stream)

ZeroMQ SUB (Bitcoin Core ZMQ)

A ZMTP 3.1 subscriber over tcp:// (NULL security) — Bitcoin Core is the PUB, this is the SUB. Each notification is multipart: [topic, body, sequence(LE u32)].

// nil topics → subscribe to everything; or pass prefix filters.
sub := nc.zmq_sub_dial("tcp://127.0.0.1:28332", []string{"hashblock", "rawtx"}) or_return
defer nc.zmq_sub_destroy(&sub)

for {
    msg, err := nc.zmq_recv(&sub)       // blocks until the next multipart message
    if err != nil { break }             // Net_Error.Connection_Closed at stream end
    defer nc.zmq_message_destroy(&msg)
    fmt.printfln("[%s] %d bytes", nc.zmq_message_topic(msg), len(nc.zmq_message_body(msg)))
}

ZMQ is best-effort — treat it as a low-latency hint and confirm authoritative state via RPC. See examples/zmq_bitcoin and BITCOIN.md.

Async (core:nbio) and threaded

// Async: drive your own nbio event loop.
nbio.acquire_thread_event_loop()
defer nbio.release_thread_event_loop()
nc.async_get("http://example.com/", on_response, &state)
nbio.run_until(&state.done)

// Threaded: a worker pool returning Futures.
tp: nc.Thread_Pool
nc.pool_start(&tp, &client, 8)
defer nc.pool_shutdown(&tp)
f := nc.pool_submit(&tp, .Get, "http://example.com/")
resp := nc.future_await(f)

Examples

Runnable programs live under examples/:

odin run examples/sync_get      -- https://example.com/
odin run examples/async_get     -- http://example.com/        # core:nbio HTTP GET
odin run examples/ws_echo       -- wss://echo.websocket.org/
odin run examples/rpc_bitcoin   -- http://user:pass@127.0.0.1:8332/
odin run examples/ws_nostr                                    # stream Nostr kind-1 (sync WS)
odin run examples/ws_nostr_async                              # same, over core:nbio (async TLS + WS)
odin run examples/sse_listen                                  # stream Wikimedia's live SSE feed
odin run examples/sse_listen_async                            # same, over core:nbio (async TLS)
odin run examples/sse_listen_threaded                         # same, on a background thread
odin run examples/zmq_bitcoin   -- tcp://127.0.0.1:28332      # Bitcoin Core ZMQ notifications

Building & testing

odin build examples/sync_get          # build an example (a buildable program)
odin check .                          # type-check the library (it has no main)

# Full local suite: the sans-I/O codecs (http1, ws, jsonrpc, http2/hpack) plus the
# integration tests (loopback HTTP/TLS/WebSocket servers, async + threaded drivers).
# Run single-threaded for fully deterministic, leak-clean results (also what CI uses):
odin test . -all-packages -define:ODIN_TEST_THREADS=1 -define:ODIN_TEST_TRACK_MEMORY=true
odin test ./http2         -define:ODIN_TEST_THREADS=1   # http2 isn't imported by root

# Optional real-network HTTPS smoke test (off by default):
NETCLIENTS_NET_TESTS=1 odin test . -all-packages -define:ODIN_TEST_THREADS=1

CI vs. local. CI (.github/workflows/ci.yml) runs the deterministic, dependency-free codec tests (http1/ws/jsonrpc/http2) plus type-check and example builds — no servers, no network. The integration suite (loopback servers, drivers) is best run locally with the command above.


Design

Two ideas carry the whole library (see CLAUDE.md for the full architecture):

  1. Sans-I/O protocol cores. TLS, HTTP/1.1, the WebSocket frame codec, the HPACK codec, and the JSON-RPC codec perform no I/O — they consume input bytes, emit events, and produce output bytes. This lets each protocol be written once and run synchronously, asynchronously, or threaded, and makes them testable against byte slices with no network (RFC spec vectors, byte-split fuzzing, adversarial inputs).

  2. Drivers move bytes. Thin per-execution-model drivers (blocking, async, threaded) shuttle bytes between a socket and the codecs and compose the TLS layer (driven through OpenSSL memory BIOs, never SSL_set_fd).

TLS verification is on by default; disabling it is explicit and named (insecure_skip_verify). Security-relevant randomness (WebSocket masking keys, Sec-WebSocket-Key) comes from an injectable RNG so it can be tested against fixed RFC vectors.

License

See repository.