Skip to content

HTTP API

This document describes the uvscope 1.x HTTP API. Event payloads and their compatibility policy are in Event schema; extension points are in Extending uvscope; deployment and recovery are in Operations.

All endpoints are same-origin JSON. Authenticated endpoints use the uvscope_session HttpOnly cookie. Errors are { "ok": false, "error": "...", "code": "..." }.

Public

Method Path Description
GET /api/health Liveness, version, libuvcpp version and setup flag
GET /api/setup/status First-run status and data directory
POST /api/setup/init Create the first administrator (only before initialization)
POST /api/auth/login Username/password login; sets the session cookie

Authenticated

Method Path Permission Description
GET /api/auth/me dashboard Current user, module flags and permissions
POST /api/auth/logout cookie Delete the session
POST /api/auth/password dashboard Change the current password and rotate the session
GET /api/auth/sessions dashboard Active sessions (admins see all, others their own)
DELETE /api/auth/sessions/:id dashboard Revoke a session (admins any, users their own)
GET /api/audit?after=&limit= admin.users Audit entries with actor, action, target, IP and time
GET /api/backup admin.settings Consistent SQLite database download (attachment)
POST /api/backup/restore admin.settings Upload a SQLite database; applied on the next daemon start
GET /api/dashboard dashboard Server/metrics/activity snapshot
GET /api/history?kind=&limit= history.read Recent activity summaries
DELETE /api/history?kind= admin.reset Clear one or all history kinds
GET /api/proxies dashboard Proxy configuration + runtime state
GET /api/proxies/:id/logs?after=&limit= dashboard Incremental proxy event log

Debug modules

Each module must be enabled in Settings and the role must have debug.

Method Path Body / result
POST /api/http/send {url, method, protocol, headers[{name,value}], body, timeoutMs, insecure, decompress, proxy} → probe result
GET /api/http/workspace Environments, active environment and saved collections
PUT /api/http/workspace Replace the workspace (validated; 2 MiB cap)
POST /api/http/download Same body as /api/http/send → background download job
GET /api/http/download/:id Download progress/state
DELETE /api/http/download/:id Cancel and delete a download (owner/admin)
GET /download/:id Staged file (public by token, attachment only)
POST /api/ws/sessions {url, headers[], subprotocol, insecure, timeoutMs, proxy} → backend WebSocket session
GET /api/ws/sessions?all= List sessions (admin all=1 sees every user)
GET /api/ws/sessions/:id Session snapshot and counters
GET /api/ws/sessions/:id/events?after=&limit= Incremental frame/message/close events
POST /api/ws/sessions/:id/send {type: text\|binary\|ping\|pong, payload, encoding}
POST /api/ws/sessions/:id/close {code, reason} → graceful Close frame
DELETE /api/ws/sessions/:id Close and remove the session
POST /api/tcp/probe {host, port, payload, encoding, connectTimeoutMs, readTimeoutMs, proxy}
POST /api/udp/probe {host, port, payload, encoding, timeoutMs, proxy}
POST /api/tcp/sessions {host, port, connectTimeoutMs, proxy} → persistent TCP session
GET /api/tcp/sessions List retained TCP sessions
GET /api/tcp/sessions/:id One session snapshot
GET /api/tcp/sessions/:id/events?after=&limit= Incremental session event log
POST /api/tcp/sessions/:id/send {payload, encoding} → queue a write
DELETE /api/tcp/sessions/:id Close a session
GET /api/transport/listeners List configured TCP/UDP listeners and runtime state
POST /api/transport/listeners {transport, name, host, port, autoStart, start}
GET /api/transport/listeners/:id One listener snapshot
POST /api/transport/listeners/:id/start Start a listener
POST /api/transport/listeners/:id/stop Stop a listener
DELETE /api/transport/listeners/:id Stop and delete a listener
GET /api/transport/listeners/:id/events?after=&limit= Listener event log
GET /api/transport/listeners/:id/sessions Accepted TCP sessions
GET /api/transport/listeners/:id/sessions/:sid/events?after=&limit= Accepted session event log
POST /api/transport/listeners/:id/send TCP: {sessionId, payload, encoding}; UDP: {peerHost, peerPort, payload, encoding}
DELETE /api/transport/listeners/:id/sessions/:sid Close one accepted TCP session
GET /api/socks5/servers List configured SOCKS5 servers and runtime state
POST /api/socks5/servers {name, host, port, username, password, autoStart, start, isolated}
GET /api/socks5/servers/:id One SOCKS5 server snapshot
POST /api/socks5/servers/:id/start Start a SOCKS5 server
POST /api/socks5/servers/:id/stop Stop a SOCKS5 server
DELETE /api/socks5/servers/:id Stop and delete a SOCKS5 server
GET /api/socks5/servers/:id/events?after=&limit= Server event log
GET /api/socks5/servers/:id/sessions CONNECT / BIND / UDP ASSOCIATE attempts
POST /api/tls/inspect {pem, encoding} → certificate profile; PEM bundles return certificates[]
POST /api/tls/probe {url, timeoutMs, insecure} → live peer chain + verification
GET /api/tls/certificates Managed certificate metadata (no private keys)
POST /api/tls/certificates/generate {name, commonName, sans[], days, keyType, keyBits} (admin)
POST /api/tls/certificates/import {name, certPem, keyPem, password} (admin)
GET /api/tls/certificates/:id Metadata plus parsed certificate chain
GET /api/tls/certificates/:id/cert Download the certificate PEM
DELETE /api/tls/certificates/:id Delete a managed certificate (admin)
POST /api/preview {html, baseUrl, mode: safe\|scripts} → {id, url} preview token (8 MiB, 120 s)
DELETE /api/preview/:id Drop a preview token
GET /preview/:id Token-based HTML with CSP: sandbox allow-scripts (no auth)
POST /api/proxies {id?, name, listenHost, listenPort, target, upstreams[], retries, connectTimeoutMs, requestTimeoutMs, workers, autoStart, tls, certPath, keyPath, rules[], start}
POST /api/proxies/:id/start Start a proxy
POST /api/proxies/:id/stop Stop a proxy
DELETE /api/proxies/:id Stop and delete a proxy

encoding is text or hex. Probe results always include ok; protocol failures are reported in the result rather than as an API error.

GET /api/health reports the build's capabilities (http2, http3, quic, brotli). Release builds keep http3 and quic false; the experimental QUIC SDK build sets both true.

/api/http/send results include streamId and retryable (0 / false on HTTP/1.1). A request without an explicit user-agent is sent with a current browser UA plus a uvscope/<version> token, so sites that serve bot or JS-redirect shims to unknown clients still return their normal HTML; an explicit header always wins.

The request body is a UTF-8 string by default. Set bodyEncoding: "base64" to send arbitrary bytes (the daemon decodes it before constructing the request); this is how the browser-built multipart/form-data body is forwarded. The same field is accepted by /api/http/download.

decompress (default true) advertises accept-encoding: gzip, deflate, br unless the request already has one, and decodes the response body. The result then carries:

"compression": {
  "requested": true,
  "contentEncoding": "br",
  "decoded": true,
  "wireBodySize": 27,
  "bodySize": 2800
}

wireBodySize is the compressed size, bodySize the decoded size. When decoding is disabled with decompress: false, the body stays compressed (bodyEncoding: "base64"), content-encoding is preserved in headers, and no compression object is returned. An unsupported encoding (for example zstd) leaves the body untouched and adds compression.error. The older compression field is accepted as an alias for decompress.

With protocol: "http2" over https://, the daemon negotiates ALPN h2 and also returns goaway: {received, errorCode, lastStreamId} plus a frame summary:

"h2": {
  "frameCount": 4,
  "framesTruncated": false,
  "maxFrames": 200,
  "frames": [
    {"type": "SETTINGS", "typeId": 4, "flags": 0, "streamId": 0, "length": 0,
     "errorCode": 0, "lastStreamId": 0},
    {"type": "HEADERS", "typeId": 1, "flags": 4, "streamId": 1, "length": 34,
     "errorCode": 0, "lastStreamId": 0}
  ]
}

frames holds the first 200 received frames (payload bytes are not captured), in arrival order; frameCount is the total seen and framesTruncated marks the cap. type uses the RFC 9113 names, flags is the raw bit mask, and errorCode / lastStreamId are meaningful only on RST_STREAM / GOAWAY. An SDK built without nghttp2 makes the daemon reject protocol: "http2" with this libuvcpp build has no HTTP/2 support.

With protocol: "http3" on an SDK built with UVCPP_ENABLE_HTTP3=ON, the result carries httpVersion: "HTTP/3", alpn: "h3", streamId, retryable and goaway: {received, lastStreamId}. libuvcpp does not expose an H3 GOAWAY error code, so errorCode is omitted rather than guessed; QPACK summaries, 0-RTT and connection-migration events are not returned. HTTP/3 cannot be combined with a SOCKS5 proxy. A build without HTTP/3 rejects the request with this libuvcpp build has no HTTP/3 support.

Large response downloads

POST /api/http/download accepts the same body as /api/http/send (the protocol, decompress and maxBodyBytes fields are ignored; the transfer is HTTP/1.1 and the bytes are stored as received). It returns immediately:

{"ok": true, "download": {
  "id": "9f2c…", "state": "running", "bytes": 0, "totalBytes": -1,
  "filename": "big.bin", "url": "/download/9f2c…"
}}

Poll GET /api/http/download/:id for state (running, ready, error, cancelled), the byte counters, resumable and attempts. When ready, GET /download/:id serves the file as HTTP 200 with Content-Disposition: attachment, Content-Length and byte-range support. The URL is public by unguessable token, but the file is always sent as application/octet-stream so target HTML can never execute on the uvscope origin.

An errored transfer keeps its partial file; resumable is true when state is error and bytes > 0. POST /api/http/download/:id/resume continues the same job (no body required) with Range: bytes=<bytes>-:

  • a 206 response must carry a matching Content-Range; the remaining bytes are appended and totalBytes is taken from the range header;
  • a 200 response means the upstream ignored Range, so the staged file is truncated and the transfer restarts from zero;
  • 416, another 4xx/5xx, or a malformed Content-Range leaves the job in error with the partial bytes intact.

A transfer that ends before the declared Content-Length is an error, not ready. attempts starts at 1 and increments on each resume. Jobs belong to their creator (admins see all), are capped at 512 MiB, can be cancelled with DELETE /api/http/download/:id, and are pruned 10 minutes after they finish. Staged files and resumable jobs do not survive a daemon restart.

QUIC (experimental)

POST /api/quic/probe takes {host, port, alpn, payload, encoding, timeoutMs, verify} and returns {ok, streamId, negotiatedAlpn, replyText | replyBase64, replySize, timing}. The client uses TLS 1.3 and does not verify the certificate by default; verify: true uses the system CA bundle and strict verification. A QUIC-enabled SDK is required; otherwise the result is {ok: false, code: "unsupported"} with the build reason.

/api/quic/servers creates and manages QUIC listeners: POST {name, host, port, alpn, certPath, keyPath, echo, start}; GET lists them; POST /:id/start|stop, DELETE /:id and GET /:id/events?after=&limit= follow the same shapes as TCP/UDP listeners. When certPath/keyPath are empty the server generates a self-signed certificate in memory. With echo: true (default) received stream bytes are written back and datagrams are echoed. Servers are not persisted across daemon restarts.

Backend WebSocket sessions

POST /api/ws/sessions connects from the daemon to a ws:// or wss:// URL. headers are added to the Upgrade request (Authorization, Cookie, Origin, Sec-WebSocket-Protocol, …); the standard handshake headers are reserved. subprotocol is a shortcut for Sec-WebSocket-Protocol, insecure disables TLS verification and proxy takes the same SOCKS5 object as the HTTP probe. Each session runs on its own thread and loop.

Snapshots carry state (connecting, open, closing, closed, error), byte/frame counters, close code/reason and nextSeq. Event types:

  • connecting, open, closing, close (code, reason), error;
  • sent (opcode, size);
  • text (text, encoding, size, truncated) and binary (base64, size, truncated);
  • frame (opcode, opcodeId, fin, rsv1, masked, length, optional text/base64, truncated, and closeCode/closeReason on CLOSE). Frames are received frames only; payloads are capped at 16 KiB in the event, while messages are capped at 256 KiB.

send accepts type: "text" | "binary" | "ping" | "pong" and encoding: "text" | "hex" | "base64" (hex/base64 are decoded before sending). close sends a Close frame with the requested code and reason; a locally initiated close settles as closed once the peer answers or after a five-second grace period. DELETE sends 1001 and hides the session immediately.

POST /api/preview stores a captured HTML document in memory (max 8 entries, 8 MiB each, 120 s TTL) and returns a random token. mode: "safe" allows remote CSS/images/fonts/frames but keeps script-src 'none'; mode: "scripts" additionally runs scripts. GET /preview/:id is public by token and always responds with Content-Security-Policy: sandbox ... in addition to the iframe sandbox, so the document runs on an opaque origin with no access to uvscope cookies, storage or API responses. The default srcdoc preview remains script-free; remote assets switch to the safe /preview document so they are not blocked by the SPA's own CSP.

Persistent TCP sessions

A session is created with POST /api/tcp/sessions and stays open until DELETE or the peer closes it. It runs on a dedicated libuv loop, so a stalled target never blocks the control plane.

{
  "ok": true,
  "session": {
    "id": "…",
    "transport": "tcp",
    "host": "127.0.0.1",
    "port": 9000,
    "state": "connected",
    "error": "",
    "closeReason": "",
    "createdAt": "2026-10-20T…Z",
    "connectedAt": "2026-10-20T…Z",
    "closedAt": "",
    "sentBytes": 10,
    "receivedBytes": 20,
    "nextSeq": 6,
    "queuedWrites": 0,
    "queuedBytes": 0
  }
}

state is connecting, connected, closing, closed or error. Sends are queued; the send event is appended only after libuv reports the write complete. Event types are created, connected, send, recv, peer-closed, closed and error; every event has seq and at. Data events carry payload with length, encoding (utf8 or base64), text or base64, hex, and truncated (payloads over 64 KiB are clipped for the event log).

Limits: 64 live sessions, 1 MiB per payload, 1 MiB queued per session, 512 KiB / 2,000 events retained per session. Closed sessions remain visible until the retained list exceeds 128.

TCP / UDP listeners

SOCKS5 outbound proxy

/api/http/send, /api/tcp/probe, /api/udp/probe and /api/tcp/sessions accept an optional proxy object:

{"proxy": {"host": "127.0.0.1", "port": 1080, "username": "", "password": ""}}
  • HTTP and TCP use SOCKS5 CONNECT; UDP uses UDP ASSOCIATE. The target hostname is sent to the proxy and never resolved locally.
  • A configured proxy is mandatory: a failed handshake fails the request and never falls back to a direct connection.
  • username empty means no-auth. A password without a username is rejected with invalid_config rather than silently downgrading to no-auth.
  • Failures include proxy.errorClass: invalid_config, proxy_unreachable, auth_failed, target_rejected, timeout, protocol_error or proxy_error, plus proxy.errorCode (libuv) and proxy.error (the proxy's own message).
  • HTTP/3 and SOCKS5 cannot be combined; that combination is rejected explicitly.

Live TLS certificate probe

POST /api/tls/probe accepts {url, timeoutMs, insecure} and connects to an https:// URL without sending an HTTP request. It returns the captured certificate profile (certificates[], leaf fields at the top level) plus live:

{"verified": true, "verifyResult": 0, "verifyError": "ok", "alpn": "", "insecure": false,
 "handshakeError": "", "certificateCount": 4}
  • The chain is returned even when verification fails (self-signed, expired, wrong host), so the UI can inspect the certificate; verified is false and verifyError carries the OpenSSL text.
  • insecure: true disables verification for the handshake itself.
  • The build requires libuvcpp 1.8.2, which provides peer_cert_chain_der.
  • HTTP/3 is not supported by this probe; ALPN is reported when the build enables HTTP/2.

POST /api/socks5/servers binds a local SOCKS5 port. Leaving username and password empty enables no-auth; providing both enables RFC 1929 username/password authentication. The password is stored as a PBKDF2 hash and is never returned. SOCKS5 servers default to 127.0.0.1 and must be moved to a LAN address explicitly. Sessions report the requested command, target, peer, status and error. The libuvcpp 1.8.2 session-end callback adds bytesClientToTarget, bytesTargetToClient, udpDatagrams, udpBytes, durationMs, endedAt and endStatus.

With isolated: true the server runs in a --socks5-child process. Its snapshot includes isolated: true and pid, and the event/session endpoints serve the cached child logs. The desired-state supervisor restarts the child if it exits unexpectedly and the snapshot reports the new pid; POST .../stop clears the desired state and POST .../start re-arms it. Deleting the server unregisters it first, then stops the child and waits for it to be reaped.

POST /api/transport/listeners binds a local port; transport is tcp or udp. TCP listeners produce accepted-session records with peer, state, byte counters and a per-session event log. UDP listeners produce datagram and sent events with peer and payload; replies use the numeric peer host/port from the datagram log.

isolated: true runs the listener in a --listener-child process. Its snapshot includes isolated and pid, and session send / close / event reads are proxied over IPC. The desired-state supervisor restarts a crashed child with a new pid; POST .../stop clears the desired state and POST .../start re-arms it. Deleting the listener unregisters it first, then stops and reaps the child.

Listener states are stopped, starting, listening, stopping and error. A listener error keeps the config; fix the port and start it again. Listener configs are stored in SQLite settings and auto-started when autoStart is true. Limits mirror the TCP session limits; accepted sessions are retained up to 128 per listener.

Managed certificates

POST /api/tls/certificates/generate creates a self-signed server certificate (RSA 2048/3072/4096 or ECDSA P-256/P-384) with SAN entries, serverAuth EKU and CA:FALSE. POST .../import accepts a PEM certificate (leaf first, intermediates optional) and a matching private key; encrypted keys are decrypted with the optional password. Files live under <data>/certs/<id>/: cert.pem, key.pem (0600) and meta.json. The API never returns key material, only local certPath/keyPath values that the reverse proxy editor can select.

generate accepts signedBy ("self", the default, or "ca"). With "ca" the leaf is signed by the local CA, cert.pem contains the leaf followed by the root, and the metadata records source: "ca". If no CA exists the API returns 409 ca_missing; an expired CA returns 409 ca_expired.

POST /api/tls/certificates/:id/renew rebuilds a generated or CA-signed certificate in place with the same id, paths, name and key type. SANs are read from the current leaf. The body accepts days (1–3650, default 365); CA-signed leaves are clamped to the remaining CA lifetime. Imported certificates return 409 not_renewable. The response contains the updated metadata including renewCount and renewedAt.

Local certificate authority

The local CA is a single root stored under <data>/ca/ (cert.pem, 0600 key.pem, meta.json):

  • GET /api/tls/ca returns { ok, exists, subject, notBefore, notAfter, daysRemaining, expired, fingerprintSha256, certPath }.
  • POST /api/tls/ca creates it once (commonName, days, default uvscope Local CA / 3650). A second call returns 409 ca_exists.
  • GET /api/tls/ca/cert downloads the root certificate as PEM.

The CA key never leaves the daemon. Deleting the vault does not remove the CA; delete the <data>/ca directory to start over.

Deleting a certificate still referenced by a proxy returns 409 certificate_in_use.

Reverse proxy routing rules

Upstream requests run on a per-instance worker pool (default 4 threads, 32 queued jobs). The listener loop only resolves rules and writes the result back, so a slow upstream does not block other requests on the same proxy. A full queue returns 503 with proxy is stopping or overloaded. Stopping a proxy waits for in-flight upstream requests before closing the listener.

Per-proxy tuning fields are stored in SQLite and applied when the proxy starts:

  • connectTimeoutMs: 100–120000, default 15000.
  • requestTimeoutMs: 100–300000, default 30000 (per upstream attempt).
  • workers: 1–16, default 4.
  • breakerThreshold: 0–100, default 5; consecutive transport failures or 5xx responses needed to open an upstream circuit. 0 disables the breaker.
  • breakerCooldownMs: 1000–300000, default 30000; how long an open circuit skips the upstream before allowing one half-open probe.
  • isolated: run this proxy in a child process. The snapshot then includes pid and telemetry is cached from the child. Stop the proxy before changing the value; a running proxy returns 409 proxy_running.

An isolated proxy is supervised: if the child exits unexpectedly the supervisor restarts it, and the /api/proxies snapshot reports the new PID. Deleting the proxy stops the child and waits for it to be reaped.

The same connect timeout applies to WebSocket upgrade attempts. Omitted fields keep the existing value on update, or the default for a new proxy.

POST /api/proxies with an existing id hot-reloads a running proxy when only name, target, upstreams, retries, the timeouts, breaker settings or rules changed: the listener and worker pool stay up, in-flight requests keep their snapshot, existing WebSocket sessions keep their upstream, and the proxy snapshot's configVersion increments with a configuration hot-reloaded log entry. Changes to listenHost, listenPort, tls, certificate paths or workers stop and restart the runtime.

POST /api/proxies accepts an ordered rules array (max 64). The first enabled matching rule wins. A rule is:

{
  "id": "optional; generated when empty",
  "name": "old docs",
  "enabled": true,
  "matchType": "exact | prefix | regex",
  "pattern": "/old",
  "method": "GET,POST",
  "host": "api.example.com",
  "headerName": "Authorization",
  "headerValue": "Bearer dev",
  "action": "redirect | rewrite",
  "status": 301,
  "destination": "/new",
  "preserveQuery": true
}
  • Matching uses the raw request path (without the query). exact compares the whole path, prefix is a byte-prefix match, regex uses ECMAScript std::regex_search.
  • method, host, headerName/headerValue are optional conditions combined with AND. method is a comma-separated, case-insensitive list. host matches case-insensitively with the port ignored unless the pattern contains one, and *.example.com matches any subdomain. headerName requires the header to exist; headerValue additionally requires an exact value. An empty pattern matches every path, so a rule can be driven by method/host/header alone.
  • redirect answers with status (301, 302, 303, 307 or 308) and a Location header without contacting the upstream. rewrite replaces the upstream path and continues proxying; the configured target path is not prepended for a rewritten request.
  • Destination variables: $uri (path), $args (query), $request_uri (path + query), $rest (remainder after a prefix match), $1–$9 (regex captures), $host (request Host), $scheme (http/https). Unknown variables are left literal.
  • With preserveQuery: true, the original query is appended unless the destination already contains ?; destinations using $args or $request_uri are not appended twice.
  • Invalid regexes, rewrite destinations that do not start with / or $, rules without any path/method/host/header condition, or more than 64 rules are rejected with 400 bad_request.

WebSocket pass-through

A reverse proxy answers WebSocket upgrades on every path and relays them to a configured upstream (target when upstreams is empty). http:// targets become ws:// and https:// targets become wss://. Upstream selection uses the same weighted list and health filtering as HTTP forwarding; retries do not apply to upgrades.

Routing rules also apply to upgrades: method, host and headerName/headerValue conditions are evaluated, and rewrite rules replace the upstream path before the handshake (preserveQuery and the destination variables behave as for HTTP). A redirect rule is skipped because the downstream 101 Switching Protocols has already been sent; the next matching rule wins. Without a rewrite, the upstream base path, request path and query are preserved.

Client headers other than hop-by-hop and Sec-WebSocket-* headers are forwarded, so Authorization, Cookie and Origin reach the upstream. Text and binary messages and close codes/reasons are relayed in both directions; the upstream TLS certificate is verified strictly. Each accepted upgrade is logged with action: "upgrade", status: 101, the selected upstream and the matched rule when one applied; relayed frames are not logged individually.

Proxy metrics

Every proxy snapshot in GET /api/proxies carries metrics computed from the last 60 seconds (up to 2048 in-memory samples, reset on daemon restart):

"metrics": {
  "windowSeconds": 60,
  "qps": 1.4,
  "p50Ms": 3,
  "p95Ms": 11,
  "status2xx": 82,
  "status3xx": 4,
  "status4xx": 1,
  "status5xx": 0
}

requests, errors and bytes remain lifetime totals for the running instance.

Upstream health

Proxies with more than one upstream run an active TCP/TLS connect probe against each upstream every 10 seconds (3-second timeout). Snapshots include a health array:

"health": [
  {"url": "http://127.0.0.1:8001", "healthy": true, "checked": true,
   "latencyMs": 2, "checkedAt": "2026-10-21T13:07:48Z", "error": "",
   "breaker": {"open": false, "failures": 0, "retryAfterMs": 0, "lastError": ""}},
  {"url": "http://127.0.0.1:8002", "healthy": false, "checked": true,
   "latencyMs": 1, "checkedAt": "2026-10-21T13:07:48Z", "error": "connection refused",
   "breaker": {"open": true, "failures": 5, "retryAfterMs": 21400,
               "lastError": "connection refused"}}
]

Healthy upstreams are preferred; an unhealthy upstream is skipped as long as at least one healthy one remains, and becomes eligible again automatically once a probe succeeds. The probe checks TCP reachability (and the TLS handshake for https://), not application-level health.

Consecutive transport failures or 5xx responses increment breaker.failures; any response below 500 resets it. Once breakerThreshold is reached, breaker.open becomes true and the upstream is skipped until breakerCooldownMs elapses (shown as retryAfterMs); the next request is a half-open probe whose success closes the circuit. When every upstream is circuit-open the proxy returns 502 {"error":"all upstreams are circuit-open"} without contacting them.

Upstreams and retries

upstreams is an optional array of {"url": "http://…", "weight": 1} objects (maximum 16, weights 1–1000). When present it replaces target for routing; when empty, target is the only upstream. Each request picks an upstream with probability proportional to its weight, and the access log plus 502 responses name the chosen upstream and the attempts count.

retries (0–3) is the number of extra attempts after the first. A failed connection retries with another upstream for any method because nothing was sent. A failed send, or an upstream responding 502/503/504, retries only for idempotent methods (GET, HEAD, OPTIONS, TRACE); other methods return the failure instead of risking a duplicate request. There is no active health checking: an upstream is tried again on the next request.

Virtual terminals

The terminal module (Unix PTY or Windows ConPTY) is disabled by default; enable it in Settings. Routes require the terminal permission (admin and operator) and an enabled module. Sessions are owned by the creating user and survive browser disconnects for the lifetime of the daemon.

Method Path Description
GET /api/terminal/sessions Own sessions; admins may pass ?all=1
POST /api/terminal/sessions {name, shell, cwd, cols, rows, isolated} → session
GET /api/terminal/sessions/:id One session (owner or admin)
DELETE /api/terminal/sessions/:id Close a session (owner or admin)
POST /api/terminal/sessions/:id/input {data, encoding} where encoding is text, hex or base64
POST /api/terminal/sessions/:id/resize {cols, rows}
GET /api/terminal/sessions/:id/output?after=&limit= Ordered output chunks, base64 encoded
WS /api/terminal/ws/:id Live stream: binary output frames, JSON control/input frames
GET /api/terminal/commands?platform=&category=&q=&all= Command catalog (seeded + own custom)
POST /api/terminal/commands Add a custom command {name, command, category, platform, description, quick}
PATCH /api/terminal/commands/:id Edit a command or toggle quick
DELETE /api/terminal/commands/:id Delete a command
POST /api/terminal/commands/restore Re-insert deleted built-in commands (admin)

Session states are starting, running, closing, exited, stopped and error. Output chunks carry seq and base64 data; nextSeq, firstSeq, truncated and missed describe the bounded scrollback (256 KiB). shell must be an absolute executable; cwd must be an absolute directory. Limits: 4 running sessions per user, 32 total.

isolated: true runs the PTY in a --terminal-child process. The parent keeps the id, owner, name, input/resize/redraw proxying and a cached 256 KiB scrollback; its snapshot adds isolated and the child pid. The child's PTY state and exit code remain authoritative.

An isolated session is supervised while it exists. If the child process dies, the supervisor spawns a fresh shell for the same session and the snapshot reports a new pid; the previous scrollback is dropped, so attached WebSocket clients receive {"type":"reset"} before the new output. A normal shell exit is not a crash: the child stays alive with status exited so the output and exit code remain readable. Deleting the session unregisters it first, then stops and reaps the child.

The daemon seeds a command catalog into SQLite on first migration (Linux and Windows, grouped by system/CPU/disk/network/process/users/files/packages/services/logs/security/containers/development). Built-ins and custom snippets are searchable by command, name, description and tags. Custom commands belong to their creator; admins can edit/delete any entry and restore deleted built-ins. quick: true pins an entry to the terminal quick bar.

The WebSocket upgrade checks the session cookie, module flag, role, terminal ownership and the Origin header. Server messages:

  • binary frames: terminal output bytes (feed them to a UTF-8 terminal emulator);
  • {"type":"attached","id":...} on attach, followed by the retained scrollback;
  • {"type":"reset"} when the client fell behind the scrollback ring;
  • {"type":"exit","state":"exited","code":0} after the process exits;
  • {"type":"error","error":"..."} for input failures.

Client messages: {"type":"input","data":"...","encoding":"text|hex|base64"}, {"type":"resize","cols":100,"rows":30} and {"type":"ping"}. The server sends SIGWINCH on attach so full-screen programs repaint.

Service supervisor

/api/supervisor lists desired-state services of every kind (proxy, listener, socks5, terminal) with desired, running, restarts, lastError and updatedAt. The watchdog polls liveness every 2 s; a desired service that is not running is started again with exponential backoff (2–60 s), and each attempt is recorded in /api/supervisor/events.

Method Path Description
GET /api/supervisor Snapshot of every supervised service (admin)
GET /api/supervisor/events?after=&limit= Restart/registration event log (admin)
POST /api/supervisor/:id/restart Stop and start a service on the daemon loop (admin)
POST /api/supervisor/:id/stop-runtime Stop the runtime but keep desired, so the watchdog restarts it (admin)

Services register when they are created or loaded at startup. Stopping a proxy, listener or SOCKS5 server through its own stop route clears desired; deleting it unregisters the entry before the runtime is removed. Terminals register only when isolated is true and stay desired until the session is deleted or the module is disabled.

Administration

Method Path Description
GET /api/settings Modules, update policy, data directory
PUT /api/settings {modules, autoUpdateCheck, updateIntervalHours}
GET /api/users List users
POST /api/users {username, password, role}
PATCH /api/users/:id {role?, enabled?, password?}
DELETE /api/users/:id Delete a user
POST /api/reset {module} where module is http, tcpudp, tls, proxy, history or all

module: "all" stops proxies, removes the SQLite database, recreates the schema and returns { "ok": true, "setupRequired": true }.

About / updates

Method Path Description
GET /api/about Version, libuvcpp/libuv, repository, cached stars and capabilities
GET /api/update/check?force=1 Latest GitHub release from Antruly/uvscope

The release check is cached for 15 minutes and can also run automatically at the configured interval. It uses only the public GitHub API and sends no project or user data.