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
| API | ESP32 device | Desktop simulator | Web playground |
|---|---|---|---|
fetch | yes (esp_http_client) | yes | yes, via the browser's fetch (CORS applies) |
WebSocket | yes | yes | yes, via 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 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:
| 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.
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: trueis ignored on the device. The connection is plaintext. TLS for raw sockets works only in the desktop simulator today.onDraindoesn't fire on the device, andbufferedAmountthere keeps the value from the lastsend.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).
- 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); 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/onConnectionreturn an unsubscribe function instead of using 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.
- fetch aborts always reject with an
AbortErrorDOMException, even forAbortSignal.timeout(ms)or a customabort(reason). Browsers reject with the signal's reason (for example aTimeoutError).
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:
.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 (reserved: not emitted yet; a failed device connect reports NET_CONNREFUSED) |
NET_ABORTED | aborted |
NET_WIFI | Wi-Fi join failed |
NET_AUTH | Wi-Fi auth failure / wrong password (reserved: not emitted yet; a failed join reports NET_WIFI) |
NET_PROTO | malformed HTTP/WS |
OOM | out 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') {
// ...
}
}