GET /v1/sandboxes/{sandbox_id}/ports/{port}/proxy/{rest}
The node side of sandbox port previews. People and agents do not call it directly: they open `https://usercontent.b3os.org/sbx/<host_id>/<port>/<token>/<rest>`, and the usercontent Worker checks the token, then forwards the request here (through the tunnel, like the edge) with the token in `x-b3os-port-token`. A path that ends at the token is redirected to the same path plus `/`, so relative URLs resolve under it. beta-api.b3os.org and beta.b3os.org never serve this route (`x-b3os-host: usercontent`). **Port token** (from `POST /v1/sandboxes/{sandbox_id}/ports/{port}/url`, or a public port's `ports[].url`): `pt1.<node>.<exp>.<sig>`. `node` is the signing node's ID (`n1`), `exp` the expiry in Unix seconds (decimal, no leading zeros) and `sig` the base64url (no padding) Ed25519 signature, made with the node key (the one that signs usage batches) over the UTF-8 bytes of ``` "b3os-port-v1\n" + node + "\n" + sandbox_id + "\n" + port + "\n" + exp ``` with `sandbox_id` in `sbx_` form (not the host-safe form) and `port` in decimal. A token is good for that sandbox and port only, until `exp`. The Worker checks the signature with the public key it holds for `node` (`verify_strict`) and the expiry before forwarding; the node checks both again, that `node` is its own ID, that the sandbox is running and that the port is declared, then connects to the port inside the sandbox over vsock. A refused token is `403 signed_url_invalid` with `reason`. **Forwarding.** The proxy forwards exactly seven methods, each declared here as its own operation: `GET` (WebSocket upgrades included), `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` and `OPTIONS` (`sandboxesProxyPortGet` … `sandboxesProxyPortOptions`). Every other method, `TRACE` and `CONNECT` included, is refused with `405 method_not_allowed` and an `Allow` header naming the seven. The app receives the request at `/<rest>`, without the `/sbx/<host_id>/<port>/<token>` prefix; `rest` may be empty (`…/proxy/` reaches the app's root), and it and the query string are passed byte for byte. The request carries `X-Forwarded-Proto: https`, `X-Forwarded-Host: usercontent.b3os.org` and `X-Forwarded-Prefix: /sbx/<host_id>/<port>/<token>`. The response comes back as the app sends it (status, headers, streamed body) minus hop-by-hop headers, `Set-Cookie`, and the two headers that would reach past the preview's own path on the shared origin, `Service-Worker-Allowed` and `Clear-Site-Data`; `Cookie` is removed from requests too (no cookies on usercontent in either direction). Unless the app sets `Referrer-Policy`, the proxy adds `Referrer-Policy: strict-origin`, so the token in the path is not sent to other sites. A response starts within 65 seconds; a WebSocket or a streamed body stays open up to an hour. **Base path.** All previews share the `usercontent.b3os.org` origin in v0.1 (per-port hostnames are planned): a page served from one port can send same-origin requests to any other usercontent URL it knows, so keep secrets out of previews. Apps should emit relative URLs. Two values describe the base path: - `X-Forwarded-Prefix` (on every forwarded request) is the request's full base path, `/sbx/<host_id>/<port>/<token>`. Build absolute paths from it. - `B3OS_PORT_BASE_PATH` (in the sandbox's environment) is only the fixed start of that path, `/sbx/<host_id>`, the same for every port and URL of the sandbox. It is not a full base path: the port and the token follow it, and the token differs per minted URL, so no value set when the sandbox starts can hold it. A public port's token lasts 7 days from sandbox creation, so its URL and base path (`ports[].url`) stay fixed; a sandbox that runs longer gets a new one.

Path Parameters

sandbox_id string required path
The sandbox ID (`sbx_…`).
Example: "sbx_01m5t68q1b590rj801nbzsqzkk"
port integer required path
A port declared in the sandbox's `ports`.
Example: 3000
rest string required path
The rest of the path after the usercontent prefix, slashes included, possibly empty (the app's root). Passed to the app as `/<rest>` byte for byte.
Example: "assets/app.js"

Header Parameters

x-b3os-port-token string required header
The port token from the usercontent path (`pt1.<node>.<exp>.<sig>`; see `sandboxesProxyPortGet`), set by the usercontent Worker. Missing, malformed, expired or not matching this sandbox and port: `403 signed_url_invalid`.
Example: "pt1.n1.1792955460.tjsgjiRC3sSYEwneW62TSvNqd-Jo_zzFI5zFYkB_YQBNl0CiBzI9xgoV6EJD_5YEmh3HbzSmb3acL8mY1LmYCg"

Responses

101 Switching protocols; the WebSocket is connected to the app's WebSocket on the port.

No response body

200 The app's response, passed through (any status, headers and body; `Set-Cookie` removed).
400 The request failed validation (`invalid_request`, `invalid_url`).
403 The port token is missing, malformed, expired, for another sandbox or port, or not signed by this node (`signed_url_invalid`).
404 No such sandbox, or the port is not declared in its `ports` (`sandbox_not_found`, `port_not_exposed`).
409 The sandbox is paused or stopped (`sandbox_not_running`).
429 A rate, concurrency or quota limit applies (`rate_limited`, `concurrency_limited`, `quota_exceeded`).
500 Something failed inside B3OS (`internal`). Retry with backoff.
502 Nothing in the sandbox accepted a connection on the port (`upstream_unreachable`), or the guest agent did not answer (`guest_unreachable`).
503 The node or the product is not available right now (`node_unavailable`, `product_unavailable`, `capacity_unavailable`).
curl -X GET 'https://beta-api.b3os.org/v1/sandboxes/sbx_01m5t68q1b590rj801nbzsqzkk/ports/3000/proxy/assets/app.js' \  -H 'Authorization: Bearer YOUR_API_TOKEN' \  -H 'x-b3os-port-token: pt1.n1.1792955460.tjsgjiRC3sSYEwneW62TSvNqd-Jo_zzFI5zFYkB_YQBNl0CiBzI9xgoV6EJD_5YEmh3HbzSmb3acL8mY1LmYCg'
const response = await fetch('https://beta-api.b3os.org/v1/sandboxes/sbx_01m5t68q1b590rj801nbzsqzkk/ports/3000/proxy/assets/app.js', {  method: 'GET',  headers: {      "Authorization": "Bearer YOUR_API_TOKEN",      "x-b3os-port-token": "pt1.n1.1792955460.tjsgjiRC3sSYEwneW62TSvNqd-Jo_zzFI5zFYkB_YQBNl0CiBzI9xgoV6EJD_5YEmh3HbzSmb3acL8mY1LmYCg"  }});const data = await response.json();console.log(data);
import requestsheaders = {    'Authorization': 'Bearer YOUR_API_TOKEN',    'x-b3os-port-token': 'pt1.n1.1792955460.tjsgjiRC3sSYEwneW62TSvNqd-Jo_zzFI5zFYkB_YQBNl0CiBzI9xgoV6EJD_5YEmh3HbzSmb3acL8mY1LmYCg'}response = requests.get('https://beta-api.b3os.org/v1/sandboxes/sbx_01m5t68q1b590rj801nbzsqzkk/ports/3000/proxy/assets/app.js', headers=headers)print(response.json())
400 Response
{  "type": "https://docs.beta.b3os.org/errors#invalid_request",  "title": "Invalid request",  "status": 400,  "code": "invalid_request",  "detail": "1 field failed validation",  "instance": "req_01m5t4yg394wegr3j4y01qb363",  "errors": [    {      "pointer": "/url",      "detail": "must be an absolute http or https URL"    }  ]}
Ask a question... ⌘I