Skip to main content

Networking

Kog gives every app its networking APIs as zero-import globals: you write await fetch(url) or new WebSocket(url) exactly as in a browser, with no import. The globals are installed by the runtime before your app runs. This page is the reference; the Networking guides are the tutorial.

The surface is intentionally close to the web platform, but narrowed to what a microcontroller can deliver. Every place it diverges is called out inline and collected in Divergences. Most divergences are backed by a test in @kog/net.

Where each API runs​

APIESP32 deviceDesktop simulatorWeb playground
fetchyes (esp_http_client)yesyes, via the browser's fetch (CORS applies)
WebSocketyesyesyes, via the browser's WebSocket
TCPSocket / UDPSocket / TCPServeryesyesthrows KogNetUnavailableError
WiFiyes (real join)emulated (always connected)emulated

CORS is real in the playground. In the web playground fetch is the browser's own, so a cross-origin request to a server without permissive CORS headers fails just as it would on any web page. (WebSocket is the browser's own too; it isn't CORS-gated, but mixed-content rules apply and the server may check the Origin header.) On a device there is no browser and no CORS, so the same code reaches any host the network routes to. See the fetch guide.


fetch​

fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>

Resolves with a Response when the response headers arrive (the body streams in after, per the standard). A non-2xx status is not a rejection — check response.ok / response.status. Rejects with a TypeError on a network/DNS/connection failure, or an AbortError if an AbortSignal fires. The abort rejection is always an AbortError DOMException, even for AbortSignal.timeout(ms) or abort(reason); browsers reject with the signal's reason instead (for example a TimeoutError).

RequestInit fields Kog honors:

FieldTypeNotes
methodstringdefault "GET"
headersHeaders | Record<string,string> | [string,string][]
bodystring | ArrayBuffer | ArrayBufferView | URLSearchParamsstrings sent UTF-8; URLSearchParams sets the form content-type
signalAbortSignalcancels the in-flight request
redirect"follow" | "error" | "manual"default "follow"

Browser-only fields (mode, credentials, cache, referrer, referrerPolicy, integrity, priority, keepalive) are accepted and ignored. They type-check so browser code ports cleanly, but a device has no origin, cookie jar, or HTTP cache. Streaming request bodies (ReadableStream) are not supported (see divergences).

const res = await fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ hello: 'world' }),
signal: AbortSignal.timeout(5000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();

Headers​

new Headers(init?: Headers | Record<string, string> | [string, string][])

append · delete · get · getSetCookie · has · set · forEach · keys / values / entries / [Symbol.iterator]. Names are case-insensitive; get combines duplicates with ", "; iteration is sorted by lowercased name. Duplicate headers (multiple Set-Cookie) are preserved. keys/values/entries return iterators, and for (const [name, value] of headers) iterates the pairs, as on the web.

Request​

new Request(input: string | URL | Request, init?: RequestInit)

Properties url · method · headers · redirect · signal · bodyUsed · body (always null — no streams). Body accessors, each usable once: .text() · .json() · .arrayBuffer() · .bytes() (→ Uint8Array) · .clone(). There is no .blob() / .formData().

Response​

new Response(body?: BodyInit | null, init?: ResponseInit)
Response.json(data, init?): Response
Response.error(): Response
Response.redirect(url, status?): Response

Properties status · statusText · ok · headers · url · redirected · type · bodyUsed · body (null). Same body accessors as Request.

AbortController & AbortSignal​

const ac = new AbortController();
fetch(url, { signal: ac.signal });
ac.abort(reason?); // rejects the fetch with AbortError

AbortSignal.timeout(ms): AbortSignal // the idiomatic fetch timeout
AbortSignal.abort(reason?): AbortSignal
AbortSignal.any(signals): AbortSignal

AbortSignal is an EventTarget: .aborted, .reason, .onabort, addEventListener('abort', …), .throwIfAborted(). Aborting a fetch cancels the underlying request, not just the JS promise.


WebSocket​

The standard browser WebSocket.

const ws = new WebSocket('wss://example.com/socket', protocols?);
ws.onopen = () => ws.send('hello');
ws.onmessage = (e) => console.log(e.data); // string, or ArrayBuffer for binary
ws.onclose = (e) => console.log(e.code, e.reason);
ws.onerror = () => {};
ws.send('text' | arrayBuffer | typedArray);
ws.close(code?, reason?);

Readystate constants WebSocket.CONNECTING/OPEN/CLOSING/CLOSED (0–3). Both the onopen/onmessage/onclose/onerror handler properties and addEventListener work. Properties: url, readyState, protocol, extensions (""), bufferedAmount (best-effort), binaryType.

binaryType defaults to "arraybuffer" (the browser default is "blob"), and "blob" is unsupported because Kog has no Blob. Binary frames therefore arrive as an ArrayBuffer.


Sockets — TCPSocket, UDPSocket, TCPServer​

A Kog-native promise + callback API. It is neither Node's net/dgram nor a browser API. Each onX(cb) registers a listener and returns an unsubscribe function. Inbound data is always an ArrayBuffer (never a string or Buffer); send-family calls also accept a string, which is UTF-8 encoded for you.

These throw KogNetUnavailableError in the web playground, because a browser has no raw sockets. Run on a device or the desktop simulator.

TCPSocket​

const s = new TCPSocket();
await s.connect(host: string, port: number, opts?: { tls?: boolean; timeoutMs?: number });
const off = s.onData((chunk: ArrayBuffer) => {}); // returns unsubscribe
s.onClose((info) => {});
s.onError((err: KogNetError) => {});
s.onDrain(() => {}); // backpressure pacing (simulator only for now)
s.send('ping' | arrayBuffer | typedArray); // queues; throws if not open
await s.close(); // graceful close (see note on device below)

Properties: readyState ("connecting" | "open" | "closing" | "closed"), remoteAddress, remotePort, localAddress, localPort, bufferedAmount, bytesRead, bytesWritten. connect resolves once established (after the TLS handshake when tls: true).

Current device limitations (the desktop simulator implements all three):

  • tls: true is ignored on the device. The connection is plaintext. TLS for raw sockets works only in the desktop simulator today.
  • onDrain doesn't fire on the device, and bufferedAmount there keeps the value from the last send.
  • close() can drop queued bytes on the device. Bytes still in Kog's send buffer may be discarded instead of flushed, so let sends drain before closing.

UDPSocket​

const u = new UDPSocket();
await u.bind(port?: number, opts?: { address?: string; reuseAddr?: boolean });
u.onMessage((msg: ArrayBuffer, rinfo: { address; port; family; size }) => {});
u.onError((err) => {});
u.send('payload' | arrayBuffer, port: number, host: string); // host: IPv4 literal or 'localhost'
u.close();

send's host must be an IPv4 address or "localhost"; a hostname fails with NET_DNS. Broadcast and multicast aren't supported yet. The reuseAddr option is accepted but the native backends ignore it.

TCPServer​

const srv = new TCPServer();
await srv.listen(port: number, opts?: { host?: string; backlog?: number });
srv.onConnection((sock: TCPSocket) => { /* an already-open TCPSocket */ });
srv.onError((err) => {});
await srv.close(); // stops accepting; existing sockets keep running

The backlog option is accepted but currently ignored by the native backends.


WiFi​

A global WiFi object. On a device it drives a real association; on the desktop simulator and web playground it is always-connected emulation (so a tutorial that gates on connectivity runs unchanged).

WiFi.connect(ssid?, password?, opts?: { timeoutMs?; bssid?; save? }): Promise<{ ssid; ip }>;
WiFi.disconnect(): Promise<void>;
WiFi.status(): { state: 'disconnected'|'connecting'|'connected'|'failed'; ssid?; ip?; rssi?; mac? };
WiFi.ready(): Promise<void>; // resolves when connected (doesn't reject on a failed join yet)
WiFi.scan(): Promise<WiFiNetwork[]>;
WiFi.on('connect'|'disconnect'|'ipchange'|'error', cb): () => void; // 'error' is reserved: no target emits it yet
WiFi.isConnected: boolean;

WiFi.connect() with no arguments joins the credentials you provisioned with kog wifi. Passing { save: true } persists the credentials to the device (off by default so a loop can't wear out flash). On a device they're currently written when the join starts, not after it succeeds. Credentials are baked into the device at flash time, never into your app bundle — see the Wi-Fi guide.

opts.timeoutMs and opts.bssid are accepted but not yet enforced on any target. To bound a join on a device, race WiFi.connect() against your own timer. On the desktop simulator and web playground, connect() resolves immediately against the always-connected emulation. A failed join rejects with NET_WIFI; a wrong password isn't reported separately yet.


Supporting globals​

The runtime also ships the web globals fetch and friends rely on, all zero-import: TextEncoder / TextDecoder, URL / URLSearchParams, btoa / atob, AbortController / AbortSignal, and a minimal Event / EventTarget / MessageEvent / CloseEvent / DOMException. TextDecoder supports UTF-8, ASCII, and Latin-1; URL is a WHATWG subset covering the common schemes (http/https/ws/wss/file + relative resolution).

const bytes = new TextEncoder().encode('héllo');
const text = new TextDecoder().decode(bytes);
const url = new URL('/path?q=1', 'https://example.com');
url.searchParams.get('q'); // "1"

Divergences from the web standard​

Kog's networking runs on a microcontroller, not in a browser. These are the places where that makes it differ from the web standard. Most items below are verified by a test in @kog/net (test/divergences.test.ts).

  1. No streaming bodies. Request.body / Response.body are always null; bodies are buffered. Use .text() / .arrayBuffer() / .json().
  2. No Blob / FormData. No .blob() / .formData(); use ArrayBuffer and URLSearchParams.
  3. fetch ignores browser-only init (credentials, mode, cache, …); a device has no origin, cookies, or cache.
  4. CORS applies only in the web playground (browser fetch); a device reaches any routable host.
  5. WebSocket.binaryType defaults to "arraybuffer", and "blob" is unsupported.
  6. WebSocket.bufferedAmount is best-effort: a lower bound, never used for correctness.
  7. Sockets are Kog-native. onData/onMessage/onConnection return an unsubscribe function instead of using Node's EventEmitter or DOM events; inbound data is ArrayBuffer.
  8. Raw sockets throw in the web playground (KogNetUnavailableError); a browser has none.
  9. WiFi is emulated off-device. The sim and playground report connected without a real radio.
  10. Minimal Event / DOMException: only what WebSocket / AbortSignal need (no capture/bubble phases).
  11. URL is a WHATWG subset; exotic IDNA/punycode host normalization is best-effort.
  12. TextDecoder is UTF-8 / ASCII / Latin-1 only; other labels throw RangeError.
  13. No HTTP/2 and no automatic decompression on device. fetch is HTTP/1.1.
  14. fetch aborts always reject with an AbortError DOMException, even for AbortSignal.timeout(ms) or a custom abort(reason). Browsers reject with the signal's reason (for example a TimeoutError).

Errors​

fetch / WebSocket throw the standard types (TypeError for network failures, DOMException for AbortError / InvalidStateError). The Kog-native surfaces (sockets, WiFi) reject or emit a KogNetError with a stable, machine-readable .code string. Branch on the string, never on a raw number:

.codeMeaning
NET_UNSUPPORTEDoperation unavailable on this target (e.g. sockets in the web playground)
NET_DNShost lookup failed
NET_CONNREFUSEDconnection refused
NET_TIMEOUTtimed out
NET_TLSTLS handshake / certificate failure
NET_CLOSEDoperation on a closed handle
NET_RESETconnection reset by peer
NET_ADDR_IN_USEaddress/port already in use
NET_UNREACHABLEhost unreachable (reserved: not emitted yet; a failed device connect reports NET_CONNREFUSED)
NET_ABORTEDaborted
NET_WIFIWi-Fi join failed
NET_AUTHWi-Fi auth failure / wrong password (reserved: not emitted yet; a failed join reports NET_WIFI)
NET_PROTOmalformed HTTP/WS
OOMout of memory, or a socket send buffer is full
try {
await s.connect('10.0.0.1', 80);
} catch (err) {
if (err instanceof KogNetError && err.code === 'NET_CONNREFUSED') {
// ...
}
}