Networking
Kog gives every app networking as global, zero-import APIs — 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 actually deliver. Every place it diverges is called out
inline and collected in Divergences — and
each divergence is backed by a test in @kog/net.
Where each API runs
| API | ESP32 device | Desktop simulator | Web playground |
|---|---|---|---|
fetch | yes (esp_http_client) | yes | yes — the browser's fetch (CORS applies) |
WebSocket | yes | yes | yes — the browser's WebSocket |
TCPSocket / UDPSocket / TCPServer | yes | yes | throws KogNetUnavailableError |
WiFi | yes (real join) | emulated (always connected) | emulated |
CORS is real in the playground. In the web playground fetch and
WebSocket are 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. On a device
there is no browser and no CORS — 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.
RequestInit fields Kog honors:
| Field | Type | Notes |
|---|---|---|
method | string | default "GET" |
headers | Headers | Record<string,string> | [string,string][] | |
body | string | ArrayBuffer | ArrayBufferView | URLSearchParams | strings sent UTF-8; URLSearchParams sets the form content-type |
signal | AbortSignal | cancels 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.
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 truly
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 — Kog has no Blob. Binary frames therefore arrive
as an ArrayBuffer.
Sockets — TCPSocket, UDPSocket, TCPServer
A Kog-native promise + callback API — not node's net/dgram, and not the
browser. 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 — 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
s.send('ping' | arrayBuffer | typedArray); // queues; throws if not open
await s.close(); // graceful flush-then-FIN
Properties: readyState ("connecting" | "open" | "closing" | "closed"),
remoteAddress, remotePort, localAddress, localPort, bufferedAmount,
bytesRead, bytesWritten. connect resolves once established (after the TLS
handshake when tls: true).
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);
u.close();
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
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
WiFi.scan(): Promise<WiFiNetwork[]>;
WiFi.on('connect'|'disconnect'|'ipchange'|'error', cb): () => void;
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 on a successful join (off by default so a loop can't
wear out flash). Credentials are baked into the device at flash time, never into
your app bundle — see the Wi-Fi guide.
opts.timeoutMs is enforced by the device's join logic (the only target with a
real radio); on the desktop simulator and web playground it is a no-op, since
connect() resolves immediately against the always-connected emulation.
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 is honest about being a microcontroller runtime, not a browser.
Each item below is verified by a test in @kog/net.
- No streaming bodies.
Request.body/Response.bodyare alwaysnull; bodies are buffered. Use.text()/.arrayBuffer()/.json(). - No
Blob/FormData. No.blob()/.formData(); useArrayBufferandURLSearchParams. - fetch ignores browser-only init (
credentials,mode,cache, …) — a device has no origin, cookies, or cache. - CORS applies only in the web playground (browser fetch/WebSocket); a device reaches any routable host.
WebSocket.binaryTypedefaults to"arraybuffer", and"blob"is unsupported.WebSocket.bufferedAmountis best-effort — a lower bound, never used for correctness.- Sockets are Kog-native —
onData/onMessage/onConnectionreturning an unsubscribe, not node'sEventEmitteror DOM events; inbound data isArrayBuffer. - Raw sockets throw in the web playground (
KogNetUnavailableError) — a browser has none. WiFiis emulated off-device — the sim and playground reportconnectedwithout a real radio.- Minimal
Event/DOMException— only whatWebSocket/AbortSignalneed (no capture/bubble phases). URLis a WHATWG subset; exotic IDNA/punycode host normalization is best-effort.TextDecoderis UTF-8 / ASCII / Latin-1 only; other labels throwRangeError.- No HTTP/2 and no automatic decompression on device — fetch is HTTP/1.1.
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 a raw number:
.code | Meaning |
|---|---|
NET_UNSUPPORTED | operation unavailable on this target (e.g. sockets in the web playground) |
NET_DNS | host lookup failed |
NET_CONNREFUSED | connection refused |
NET_TIMEOUT | timed out |
NET_TLS | TLS handshake / certificate failure |
NET_CLOSED | operation on a closed handle |
NET_RESET | connection reset by peer |
NET_ADDR_IN_USE | address/port already in use |
NET_UNREACHABLE | host unreachable |
NET_ABORTED | aborted |
NET_WIFI | Wi-Fi join failed |
NET_AUTH | Wi-Fi auth failure / wrong password |
NET_PROTO | malformed HTTP/WS |
try {
await s.connect('10.0.0.1', 80);
} catch (err) {
if (err instanceof KogNetError && err.code === 'NET_CONNREFUSED') {
// ...
}
}