A bi-directional binary req/res and streaming framework over WebSocket.
Messages are serialised with CBOR by default and the encoding is negotiable per-response. Streams use a WHATWG-aligned credit-based backpressure protocol multiplexed over the same connection.
npm install viae
Create a Viae instance by passing any object that implements WireServer (emits "connection" events with a Wire). The built-in WebSocketWire adapts the ws library or a browser WebSocket.
import { WebSocketServer } from "ws";
import { Api, Viae, WebSocketWire } from "viae";
import type { Wire } from "viae";
const wss = new WebSocketServer({ port: 8080 });
// Adapt the ws server so Viae can accept connections
const wireServer = {
on(event: "connection", cb: (wire: Wire) => void) {
wss.on("connection", (ws) => cb(WebSocketWire.wrap(ws)));
}
};
const viae = new Viae(wireServer);
const api = new Api("/");
api.get({
path: "/hello/:name",
handler: ({ params }) => `Hello, ${params.name}`,
});
viae.use(api);import WebSocket from "ws";
import { Via, WebSocketWire } from "viae";
const ws = new WebSocket("ws://localhost:8080");
const wire = WebSocketWire.wrap(ws);
const via = new Via({ wire });
await via.ready;
const result = await via.request<string>("GET", "/hello/world");
console.log(result.data); // "Hello, world"
wire.close();Api is a method-based router. Mount it on a Viae instance (server-side) or a Via instance (client-side).
import { Api, Viae } from "viae";
const api = new Api("/");
api.get({
path: "/hello/:name",
handler: ({ params }) => `Hello, ${params.name}`,
});
viae.use(api);Supply a custom context interface as a type argument to get full type-safety for properties you attach during the request lifecycle:
interface AppContext extends Context {
userId: string;
connection: IVia<{ sub: string }>;
}
const api = new Api<AppContext>("/");
// guard — runs for every request under this router
api.use("*", async (ctx, next) => {
ctx.userId = ctx.connection.state.sub; // typed claims, no cast
return next();
});
api.get({
path: "/profile",
handler: ({ ctx }) => ({ id: ctx.userId }),
});api.use(path, handler) registers a middleware that runs in definition order before any route handler whose path starts with path. Use "*" to match all paths under the router:
api.use("*", async (ctx, next) => {
if (!ctx.connection.state) throw new ViaeError(Status.Unauthorized, "unauthenticated");
return next();
});
api.use("/admin", async (ctx, next) => {
if (!isAdmin(ctx)) throw new ViaeError(Status.Forbidden, "admins only");
return next();
});You can also nest a sub-Api as before:
root.use("/api", nested);| Option | Type | Description |
|---|---|---|
path |
string |
Path pattern (path-to-regexp v6 syntax) |
handler |
function |
Called with { data, head, raw, path, params, ctx } |
params |
ParamsSchema |
Per-param type descriptors — coerces and types path params (see below) |
validate |
(v) => v is R |
Type guard run before handler; rejects with 400 on failure |
accept |
"object" | "stream" |
Expected data shape — "stream" receives a ReadableStream. Omitted accepts either shape |
end |
boolean |
Whether the match must be terminal (default true) |
next |
boolean |
When true, next() is passed to the handler as next. Default false — the handler receives no next |
Methods available: api.get, api.post, api.put, api.delete, api.all, api.subscribe.
Path parameters are strings by default. Supply a params schema to coerce them at runtime and have them typed correctly in the handler:
api.get({
path: "/items/:id",
params: { id: { type: Number } },
handler: ({ params }) => params.id * 2, // params.id is typed as number
});
api.get({
path: "/flag/:enabled",
params: { enabled: { type: Boolean } },
handler: ({ params }) => params.enabled, // typed as boolean; "true"/"1" > true
});Supported types:
type |
Coercion | Rejects with 400 if |
|---|---|---|
Number |
Number(raw) |
!Number.isFinite(n) — NaN, Infinity, and -Infinity are rejected; finite forms such as "0x10" and "1e3" pass |
Boolean |
"true" / "1" > true, anything else > false |
— |
String |
no-op (default) | — |
Routers are composable. Parameters captured in a parent prefix are available in child handlers.
const root = new Api("/foo/:id");
const sub = new Api("/");
sub.get({
path: "/bar",
handler: ({ params }) => `id is ${params.id}`,
});
root.use("/", sub);
viae.use(root);
// GET /foo/42/bar > "id is 42"api.post<number>({
path: "/double",
validate: (v): v is number => typeof v === "number",
handler: ({ data }) => data * 2,
});For accept: "stream" routes, validate runs lazily per chunk (through a
TransformStream). A failing chunk errors the response stream mid-flight
instead of returning 400, because the response status has already been sent
once the stream is established.
Instead of returning a value, you can call ctx.reply() directly to set the response data, status code, and encoding simultaneously:
api.get({
path: "/info",
handler: ({ ctx }) => {
ctx.reply({ version: 1 }, { status: 200, type: "json" });
},
});| Option | Type | Description |
|---|---|---|
type |
string |
Encoding name: "cbor" (default), "json", "binary", or any custom encoder registered in the codex |
status |
number |
Response status code |
If both ctx.reply() and a return value are present the return value takes precedence for data.
Throw ViaeError to send a specific status code back to the caller:
import { ViaeError, Status } from "viae";
api.get({
path: "/secret",
handler: ({ ctx }) => {
if (!ctx.connection.state) throw new ViaeError(Status.Unauthorized, "unauthenticated");
return "classified";
},
});ViaeError responses keep the status and message you throw, wherever the error
originates (handler, guard, or middleware). Any other error becomes
500 "internal error" for the caller — the full detail is logged locally and
never sent to the peer.
Return a ReadableStream from a handler to stream chunks to the consumer. The framework multiplexes it over the existing connection using a credit-based backpressure protocol. Credits are set (not additive) — on each PULL the consumer tells the producer its current capacity, so the producer always has an accurate view of consumer capacity. The default window is 32 chunks.
api.get({
path: "/numbers",
handler: () =>
new ReadableStream<number>({
start(controller) {
for (let i = 0; i < 100; i++) controller.enqueue(i);
controller.close();
},
}),
});Request with accept: "stream" to receive a ReadableStream. The type
argument is the chunk type:
const result = await via.request<number>(
"GET", "/numbers", undefined, { accept: "stream" }
);
const reader = result.data!.getReader(); // ReadableStream<number>
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value); // number
}Or pipe through the WHATWG Streams API:
await result.data!.pipeTo(
new WritableStream({ write: (chunk) => console.log(chunk) })
);Legacy call sites that pass the stream type itself keep their exact meaning:
via.request<ReadableStream<number>>("GET", "/numbers", undefined, { accept: "stream" })
still types data as ReadableStream<number>, and a variable typed as plain
RequestOptions still compiles. IVia.request itself stays non-overloaded:
interface-typed consumers keep the request<R> signature.
When accept is supplied it is a validated assertion: "stream" rejects the request when the response is an object, and "object" rejects it when the response is a stream. Omit accept to accept either shape:
const stream = await via.request("GET", "/numbers", undefined, { accept: "stream" });
// rejects with "expected stream response but received object" if the server replies with an objectBecause omitted accept stays permissive at runtime (either shape is
accepted), data is typed as the bare type argument in that case — pass
accept when you need the precise shape.
Cancelling the consumer propagates back to the producer:
await reader.cancel(new Error("no longer needed"));
// > producer's ReadableStream cancel(reason) is called with the reasonA producer error propagates forward to the consumer:
// server side
api.get({
path: "/fail",
handler: () =>
new ReadableStream({ start(c) { c.error(new Error("oops")); } }),
});
// client side
await assert.rejects(() => reader.read(), /oops/);Cancellation and error reasons cross the wire as strings: an Error is reduced
to its message, and the opposite side receives that string — never the
original Error object — for both consumer cancels and producer errors.
For byte-oriented streams, opt into the pass-through binary codec. This keeps each chunk as raw octets instead of wrapping it in CBOR:
const result = await via.request<Uint8Array>(
"GET", "/blob", undefined, { accept: "stream", encoding: "binary" }
);On a response, set the encoding when replying:
api.get({
path: "/blob",
handler: ({ ctx }) => ctx.reply(byteStream, { status: 200, type: "binary" }),
});The default remains CBOR for compatibility. StreamOptions.encoding can also
be used to set the default for outgoing stream chunks. Inbound streams are
bounded by default — 1024 queued chunks and 64 MiB per stream — with Infinity
as an explicit opt-out. strictProtocol and cancelIncomingOnDispose remain
opt-in protocol and ownership controls.
Pass streamOptions to Viae (applies to every server connection) or Via:
| Option | Type | Default | Description |
|---|---|---|---|
highWaterMark |
number |
32 |
Initial credit window (in chunks) for incoming streams. |
maxQueuedChunks |
number |
1024 |
Max inbound chunks buffered outside the WHATWG stream queue before the stream aborts. Infinity opts out. |
maxQueuedBytes |
number |
64 MiB |
Approximate inbound byte cap. Binary chunks are counted exactly; other chunks use the encoded data-segment length. Infinity opts out. |
encoding |
string |
"cbor" |
Default chunk encoding when the enclosing message does not specify one. |
strictProtocol |
boolean |
false |
Strictly validate stream control frames; permissive fallbacks otherwise. |
cancelIncomingOnDispose |
boolean |
false |
Cancel an incoming request body when its request context is disposed. |
startTimeout |
number |
3000 |
Max ms the producer waits for the consumer's initial credit. 0 disables. |
producerIdleTimeout |
number |
0 |
Max ms the producer may go without progress (chunk sent or credit received) before aborting the stream. 0 disables, so indefinite backpressure remains the default. |
idleTimeout |
number |
0 |
Max ms a consumer read waits for the next chunk. 0 disables. |
maxCredit |
number |
Infinity |
Upper bound on stream credit — the chunks a producer may have in flight at once. The consumer clamps the credit it requests (START/PULL); the producer fails the stream fast with a protocol error when a peer grants more. Configure the same value on both peers. |
Important:
maxCreditmust be configured on both peers. A server-only configuration fails a default client's firstSTART— a 32-credit grant from the default 32-chunk window — withgranted credit 32 exceeds maxCredit N: the producer cancels the source and sends an error frame. A producer-only configuration fails the same way once a default consumer grants 32.
Both Viae and Via extend Rowan<Context>, so arbitrary middleware can be inserted into the processing pipeline. viae depends on rowan but does not re-export it — import Rowan, Middleware, Processor, or Next from rowan directly when you need those types:
viae.use(async (ctx, next) => {
const start = Date.now();
await next();
ctx.log.info(`${ctx.in.head.method} ${ctx.in.head.path} ${Date.now() - start}ms`);
});before() registers middleware that runs before the main pipeline — useful for auth or tracing:
viae.before(async (ctx, next) => {
if (!ctx.connection.state) {
ctx.out!.head.status = Status.Unauthorized;
return;
}
return next();
});The Codex is a registry of named encoders. The default codex ships three:
| Name | Description |
|---|---|
"cbor" |
CBOR via cbor-x (default) |
"json" |
JSON via JSON.stringify / JSON.parse |
"binary" |
Pass-through for ArrayBuffer / ArrayBufferView |
The encoding is negotiated per-response via head.encoding. Pass encoding in SendOptions or use ctx.reply({ type: "json" }) on the server side:
// explicit encoding on a via.send() call
await via.send({ id: "x", head: { method: "GET", path: "/blob" } }, { encoding: "binary" });Register custom encoders by extending the codex:
import { defaultCodex, FrameEncoder } from "viae";
const myCodex = {
...defaultCodex,
msgpack: {
encode: (v) => /* ... */,
decode: (b) => /* ... */,
},
};
const via = new Via({ wire, codex: myCodex });await via.send({ id: "abc", head: { method: "CUSTOM", path: "/" } });PING, PONG, START, PULL, CANCEL, and COMPLETE are reserved control methods: they are consumed internally for heartbeat and stream control and are not valid request methods.
| Option | Type | Description |
|---|---|---|
wire |
Wire |
Required. The underlying transport. |
state |
S (inferred) |
Typed per-connection state/claims snapshot. When present — including an explicit undefined — it overrides wire.state. Resolved once at construction; later wire.state mutations are ignored. Infers S on new Via(...); defaults to unknown. |
uuid |
() => string |
Id generator for requests, responses, and streams. Defaults to shortId. |
log |
Log |
Logger instance. Defaults to Via.Log (console). |
timeout |
number |
Request timeout in ms (default 120000). |
codex |
Codex |
Named encoder registry. Defaults to defaultCodex (cbor, json, binary). |
frameOptions |
FrameEncoderOptions |
Frame limits. maxFrameSize defaults to 64 MiB (64 * 1024 * 1024); opt out with Number.MAX_SAFE_INTEGER. |
streamOptions |
StreamOptions |
Stream-layer options forwarded to each stream (queue caps, timeouts, encoding). See Stream options. |
heartbeat |
{ interval?: number; timeout?: number } |
Protocol-level keepalive. Off by default (no timers). When enabled, sends reserved PING frames every interval (default 15000 ms) and requires liveness proof within timeout (default 5000 ms). Any well-formed inbound frame counts as proof; a miss is a connection failure — reconnects when configured, otherwise permanent close plus wire close. |
reconnect |
{ wire: () => Wire | Promise<Wire>; minDelay?: number; maxDelay?: number; factor?: number; jitter?: number; maxAttempts?: number } |
Client-only reconnect policy. Off by default. Defaults: minDelay 100, maxDelay 10000, factor 2, jitter 0.2, maxAttempts Infinity. On unexpected loss, in-flight requests reject (never replayed), streams abort, disconnect fires, and a replacement wire is built from wire() with jittered exponential backoff. Permanent protocol failures never retry; ready waits through reconnects and rejects only on permanent closure. |
maxInflightRequests |
number |
Maximum concurrent non-reserved inbound requests. Over-cap frames get 503 Busy without running any handler. Default 0 (disabled). |
maxStreamsPerConnection |
number |
Maximum concurrent multiplexed streams, in both directions. Over-cap streams are refused with 503 Busy. Default 0 (disabled). |
maxBufferedBytes |
number |
Wire backpressure threshold. Only applies when the wire reports a finite bufferedAmount; outbound non-control sends wait until it drains to or below the threshold. Control PING/PONG bypass the wait. Default 0 (disabled). |
protocolVersion |
number |
Protocol major version, emitted as v on non-empty heads. An inbound v must match or the connection fails permanently; 0 disables emission and validation. Default 1; overrides frameOptions.protocolVersion. |
Viae accepts the same frameOptions, codex, and streamOptions settings and
forwards them to each connection. Its timeout option sets the default request
timeout for every Via connection it creates.
| Option | Type | Description |
|---|---|---|
log |
Log |
Logger for the server and every connection it creates. Defaults to Viae.Log (console). |
middleware |
Processor<Context>[] |
Middleware applied to every inbound connection. |
timeout |
number |
Default request timeout in ms for each server-created Via (default 120000). |
frameOptions |
FrameEncoderOptions |
Frame limits forwarded to each connection (maxFrameSize defaults to 64 MiB). |
codex |
Codex |
Named encoder registry forwarded to each connection. |
streamOptions |
StreamOptions |
Stream-layer options forwarded to each connection. See Stream options. |
heartbeat |
{ interval?: number; timeout?: number } |
Heartbeat policy forwarded to each connection. Defaults 15000 / 5000 ms; see Via options. |
maxConnections |
number |
Maximum concurrent connections. 0 (default) is unlimited; excess connections are closed immediately, without creating a Via or emitting connection. |
maxInflightRequests |
number |
Forwarded to each connection (default 0, disabled). See Via options. |
maxStreamsPerConnection |
number |
Forwarded to each connection (default 0, disabled). See Via options. |
maxBufferedBytes |
number |
Forwarded to each connection (default 0, disabled). See Via options. |
protocolVersion |
number |
Forwarded to each connection (default 1). See Via options. |
heartbeat, maxInflightRequests, maxStreamsPerConnection,
maxBufferedBytes, and protocolVersion all apply to every connection Viae
creates; reconnect is client-only — there is no server-side reconnect policy.
Viae and Via each have a static Log property that defaults to a console-backed implementation. Supply any object matching the Log interface (trace, debug, info, warn, error, fatal) to redirect output:
import { Viae } from "viae";
// bring your own logger - anything with trace/debug/info/warn/error/fatal methods
Viae.Log = myLogger; // applies to all future connectionsOr pass per-instance:
const viae = new Viae(wireServer, { log: myLogger });Via.close(opts?) closes a connection. drain defaults to false: pending
requests are rejected immediately and the wire is closed. With drain: true,
active handlers and their tasks are allowed to finish, bounded by
drainTimeout (default 5000 ms); on expiry any remaining streams are
force-aborted and the wire is closed. close() is idempotent — every call
returns the first call's promise.
await via.close({ drain: true, drainTimeout: 5000 });
console.log(via.closed); // trueVia.closed flips to true on permanent closure; via.ready resolves when
the wire is open, waits through transient drops, and rejects only on permanent
closure.
Viae.close(opts?) marks the server closed — new connections are refused and
closed immediately, without creating a Via or emitting connection — then
drain-closes every connection current at call time and resolves once they have
all settled. drainTimeout is per connection (default 5000 ms). It is
idempotent, and Viae.closed becomes true.
Viae does not own the underlying WireServer/http server socket — callers
must close it themselves. Draining only ends the accepted connections.
| Event | Fires |
|---|---|
open |
The wire reached OPEN (also after a reconnect; not emitted if the wire is already OPEN when bound). |
close |
Permanent closure only — an unexpected loss with no reconnect policy, attempts exhausted, a permanent protocol failure, or an explicit close(). |
disconnect |
A transient drop: an unexpected connection loss while a reconnect policy is configured. Without a policy the loss is permanent and only close fires. |
reconnected |
A replacement wire was built and reached OPEN. |
error |
A wire error, an unhandled processing error, or the reason for an unexpected drop. |
An intentional close() emits close but never disconnect.
Optional per-connection caps reject excess work with 503 Busy
(Status.Busy):
maxInflightRequests— concurrent non-reserved inbound requests. Over-cap requests get503without running any handler.maxStreamsPerConnection— concurrent multiplexed streams, in both directions. Over-cap streams are refused with503(a best-effortCANCELstops a peer that already announced the stream).maxBufferedBytes— outbound non-control sends wait while the wire reports abufferedAmountabove the threshold;PING/PONGbypass the wait.
No heartbeat timers run by default. When heartbeat is configured, each side
sends a reserved PING every interval and requires a liveness proof within
timeout; any well-formed inbound frame settles the beat. PING is answered
with PONG even by peers that have no heartbeat configured, so
heartbeat-capable peers can probe legacy ones — a legacy peer's normal 404
also proves liveness. A miss is a connection failure: it reconnects when a
policy is configured, otherwise it is a permanent close plus wire close. See
Via options for defaults.
reconnect is a client-only policy, off by default. On an unexpected loss,
in-flight requests reject and are never replayed, streams abort, disconnect
fires, and a replacement wire is built from wire() with jittered exponential
backoff. Permanent protocol failures never retry, and ready waits through
reconnects, rejecting only on permanent closure. See
Via options for the backoff defaults.
By default non-empty heads carry v: 1. An inbound head carrying a v must
match the configured major or the connection fails permanently; heads without
v are accepted for legacy peers, and empty-head frames carry no v. Set
protocolVersion: 0 to disable both emission and validation.
A wire may be bound to only one Via. Constructing a second, different Via
on the same wire throws wire is already bound to another Via. The same Via
may re-claim a replacement wire during reconnect.
WebSocket handshakes are HTTP requests, so authentication is owned by the
WireServer adapter, not by viae. The model has two cooperating layers:
- Admission (upgrade-time). The adapter authenticates the handshake and,
on success, attaches the resulting identity to
wire.statebefore handing the wire to viae. On failure the wire is closed and never handed off. - Authorization (per request). Guards read the snapshot from
ctx.connection.state(Via.state) and check it against the request.
import { WebSocketServer } from "ws";
import { Api, Status, Viae, ViaeError, WebSocketWire, type Wire } from "viae";
const wss = new WebSocketServer({ port: 8080 });
const wireServer = {
on(event: "connection", cb: (wire: Wire) => void) {
wss.on("connection", (ws, req) => {
const claims = authenticate(req); // your auth logic (cookies, header, ticket, ...)
const wire = WebSocketWire.wrap(ws, req);
if (!claims) {
wire.close(1008, "unauthorized"); // reject: never hand off
return;
}
wire.state = claims; // accepted: claims ride along with the connection
cb(wire);
});
},
};
const api = new Api("/");
api.use("*", async (ctx, next) => {
const claims = ctx.connection.state as { sub: string } | undefined;
if (!claims) throw new ViaeError(Status.Unauthorized, "unauthenticated");
return next();
});
api.get({ path: "/whoami", handler: ({ ctx }) => ctx.connection.state });
const viae = new Viae(wireServer);
viae.use(api);wire.state is opaque to viae and may be any value. Set it before handoff:
Via snapshots state once at construction, resolving it by presence — a
ViaOptions.state (even an explicit undefined) overrides wire.state.
Later mutations of wire.state are ignored by an existing via, including
across reconnect rebinds, so make state a stable object and mutate it in
place if you need to update claims. Viae itself has no state option: the
adapter is the single admission point.
wire.state stays unknown (viae never reads it), but the Via layer is
generic over the claims shape: Via<S>, ViaOptions<S>, and IVia<S>. Narrow
connection in a context interface and hand that interface to Api:
interface Claims {
sub: string;
scopes: string[];
}
interface AppContext extends Context {
connection: IVia<Claims>;
}
const api = new Api<AppContext>("/");
api.use("*", async (ctx, next) => {
ctx.connection.state.sub; // string
// ctx.connection.state.nope; // type error: unknown claim key
return next();
});IVia<S> declares state as required (a breaking change in this beta):
structural IVia mocks must add a state member. On the client side,
ViaOptions.state infers S directly from the option:
const via = new Via({ wire, state: { sub: "alice", scopes: ["read"] } });
via.state.sub; // stringViaOptions.state still overrides wire.state by presence (an explicit
undefined wins), and the snapshot is still resolved once at construction.
Wire member |
Description |
|---|---|
state |
Adapter-set per-connection state/claims, opaque to viae. Set before handoff. |
upgrade |
Readonly upgrade metadata passed to WebSocketWire.wrap(ws, upgrade), e.g. the ws request. undefined on client connect() wires. |
close(code?, reason?) |
Forwards to the transport per argument. The first call wins; later calls while CLOSING are no-ops. |
Rejecting a connection. Close the wire with wire.close(code, reason) —
1008 (policy violation) or an application code in 4000-4999 (e.g. 4401)
is recommended for server-side rejections. RFC 6455 close bodies require a
status code, so transports (including ws) drop a reason sent without a code;
always pass both. viae never validates or rewrites codes.
Upgrade metadata. WebSocketWire.wrap(ws, req) stores its second argument
on the readonly wire.upgrade, so adapters can pass the ws upgrade request
(headers, URL, socket) through without closing over it. viae never reads it.
Revocation and expiry are user-land. WebSocket connections are stateful, so
viae ships no revocation machinery: via.state is a snapshot and cannot by
itself notice a revoked session. Recommended patterns:
- Re-validate claims per request. Guards check the claims against the
authoritative session store (or an expiry timestamp inside the claims) on
every request — the only way a long-lived connection observes a revocation.
You can also schedule
wire.close(1008, "expired")at the claim's expiry. - Drop a connection.
via.close()for a drained close, orctx.connection.wire.close(1008, "revoked")/wire.close(4401, "revoked")for an immediate one. - Sweep. Iterate
viae.connections(or listen toViae.on("connection")), inspectvia.state, and close the sessions that are revoked — useful for a server-wide "log out everywhere" action. - Reconnect re-authenticates server-side, not client-side. A client
reconnectperforms a fresh handshake, so the server runs its adapter gate again and the new server-sideViasnapshots freshwire.state. The client's ownVia.stateis developer-supplied and does not refresh on rebind: refresh it by mutating the shared state object or constructing a newVia; reconnecting alone does not refreshvia.state.
Security notes.
- Allowlist the handshake
Originin the adapter: WebSocket handshakes are not subject to CORS, so any web page can attempt a cross-origin connection. - Cookies are sent on WebSocket handshakes, but
SameSitedoes not reliably protect them across schemes/ports; prefer explicit credentials. - Tokens in query strings leak to access logs, proxies, and browser history.
- Node clients can send an
Authorizationheader; browsers cannot set custom headers onWebSocket, so browser auth usually rides on cookies or an out-of-band token. headis message metadata, not connection identity: do not smuggle tokens throughctx.in.head. Put the upgrade identity inwire.stateand read it fromctx.connection.state.
Secure defaults are applied on import:
- Frames are limited to 64 MiB by default, inbound and outbound, via
FrameEncoderOptions.maxFrameSize. Opt out withframeOptions: { maxFrameSize: Number.MAX_SAFE_INTEGER }. - The cbor-x decoder is initialised with size limits (
maxArraySize1,000,000 /maxMapSize100,000 /maxObjectSize100,000) whenviaeis imported, rejecting decoder-amplification payloads such as truncated array-32 headers. Note that cbor-x size limits are process-global: importingviaechanges decoding limits for every cbor-x consumer in the process. - Inbound streams are bounded to 1024 queued chunks and 64 MiB per stream by
default; either bound can be opted out of with
InfinityviastreamOptions. The consumer aborts an over-limit stream and best-effort cancels the producer. - Concurrency caps (
maxInflightRequests,maxStreamsPerConnection) and wire backpressure (maxBufferedBytes) are available per connection; seeViaoptions and Caps.
import { Status } from "viae";
Status.OK // 200
Status.Partial // 206 (stream chunk)
Status.BadRequest // 400
Status.Unauthorized // 401
Status.Forbidden // 403
Status.NotFound // 404
Status.Error // 500
Status.Busy // 503MIT