Runtime API

Proxy

Reach an HTTP or WebSocket server running on a loopback port inside the workspace (for example a dev server on 127.0.0.1:3000) as if you were talking to it directly. Request and response headers, streaming/SSE bodies, and WebSocket upgrades all pass through near-natively.

Requires the internal server to be enabled.

The proxy is loopback-only. Targets must resolve to 127.0.0.1 / localhost / ::1. Requests to any other host are rejected with 400 — the proxy cannot be used to reach the workspace's private network or the internet.

Overview

1. A service listens on 127.0.0.1:<port> inside the workspace (e.g. a dev server)
2. Call /proxy with the target port    →  runtime forwards to 127.0.0.1:<port>
3. HTTP response / stream / WS frames   →  passed back through untouched

The runtime connection token is used to authenticate the request to /proxy and is stripped before the request reaches the in-VM app, so it never leaks to code running inside the workspace.


Target resolution

The target port is selected by the first of these that is present:

SourceFormNotes
HeaderX-Oblien-Proxy-Target: <port> or <host>:<port>Preferred for HTTP. Path passes through untouched.
Queryx_oblien_target=<port> or <host>:<port>For browsers / WebSocket, which cannot set request headers.
Path/proxy/<port>/<upstream path>Convenience form; the port segment is stripped.

When the header or query form is used, the upstream path is the request path with the leading /proxy removed. The host, if given, must be a loopback address. token and x_oblien_target are reserved query params and are removed before forwarding upstream.


HTTP request

Forward an HTTP request to a loopback port. The SDK returns the raw Response so you get the upstream status, headers, and body (including streams).

const rt = await client.workspaces.runtime('ws_a1b2c3d4');

// GET → raw fetch Response
const res = await rt.proxy(3000).fetch('/api/users?limit=10');
const users = await res.json();

// POST with a body
await rt.proxy(3000).fetch('/api/users', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ name: 'ada' }),
});
GET https://workspace.oblien.com/proxy/api/users?limit=10
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
X-Oblien-Proxy-Target: 3000

Equivalent path form (no header):

GET https://workspace.oblien.com/proxy/3000/api/users?limit=10
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# Header form
curl "https://workspace.oblien.com/proxy/api/users?limit=10" \
  -H "Authorization: Bearer $GATEWAY_JWT" \
  -H "X-Oblien-Proxy-Target: 3000"

# Path form
curl "https://workspace.oblien.com/proxy/3000/api/users?limit=10" \
  -H "Authorization: Bearer $GATEWAY_JWT"

The response is returned verbatim — status code, headers, and body. Streaming responses (chunked, Server-Sent Events) are flushed through immediately.

Streaming (SSE)

const stream = await rt.proxy(8080).fetch('/events');
for await (const chunk of stream.body!) {
  process.stdout.write(new TextDecoder().decode(chunk));
}

WebSocket

Open a WebSocket to a loopback port. Auth and target are passed as query params (token, x_oblien_target) because the browser WebSocket API cannot set request headers.

const ws = rt.proxy(3000).ws('/socket');

ws.onmessage = (e) => console.log(e.data);
ws.onopen = () => ws.send('hello');
wss://workspace.oblien.com/proxy/socket?token=<GATEWAY_JWT>&x_oblien_target=3000

Error responses

StatusMeaning
400No target given, invalid port, or a non-loopback host was requested
401Missing or invalid token
502Nothing is listening on 127.0.0.1:<port>, or the upstream connection failed