Skip to content
shippy

HTTP API

This page lists every HTTP route shippyd serves, who may call it and what it answers. You need it to write a monitoring check, debug a client against a box, or read the box's logs. You never call these routes to use shippy: the shippy client does it for you.

Bodies are JSON unless noted. Digests and keys are 64 hex characters. Terms used below:

  • Pre-key: a hash of what a compilation reads that is known before rustc runs (toolchain, arguments, environment, compiled inputs).
  • Manifest: the source files and env! variables a compilation read, with their content.
  • Action key: the pre-key and a manifest together. A result is stored under it.
  • Blob: a file stored under its own BLAKE3 digest.
  • Claim: a result an untrusted principal uploaded, seen only by that principal until the box re-runs it.
  • Pack: the list of results a whole build used, stored per build id (a name for "a build like this one").

The glossary has more.

A miss, then a hit

Loading diagram...

A wrapper that misses locally asks twice: lookup for the manifests under its pre-key, each with its result, then blobs/fetch for the outputs it lacks. Machine A's upload is a claim if A's token is untrusted: Machine C only gets it once the box has re-run it, or immediately if A is trusted. See the wire and trust.

Authentication

Every /v1/ route needs Authorization: Bearer <token>. A missing or unknown token gets 401 with WWW-Authenticate: Bearer.

Token roleRoutes it reaches
trusted, untrustedthe client routes below, and /metrics
metrics/metrics only
worker, trusted-workerthe /v1/worker/* routes only

/healthz and /readyz need no token. See token roles.

Client routes

For trusted and untrusted tokens.

Method and pathBodyAnswer
GET /v1/lookup/{pre_key}noneThe manifests under the pre-key, newest first, each with the result the caller may see.
POST /v1/blobs/fetchlist of blob asksOne stream of frames, zstd if asked.
GET /v1/pack/{build_id}noneThe build's pack: what the caller and others listed. 304 or 404.
PUT /v1/pack/{build_id}list of pack entries204. 413 past 20,000 entries.
GET /v1/manifests/{pre_key}noneThe manifests under the pre-key, newest first.
GET /v1/action/{key}noneThe result for the action key, or 404.
PUT /v1/action/{key}recipe and resultstored_as (the trust it was stored with) and queued_for_verification.
POST /v1/blobs/missinglist of blob asksThe digests the caller must send.
POST /v1/blobs/putframes, raw or zstdstored and refused digests, each once. 400 past 1,024 refused blobs.
GET /v1/blob/{digest}noneThe blob's raw bytes. ?key=<action key> names a result that lists it.
PUT /v1/blob/{digest}raw bytes204. 400 when the bytes are not that digest.
POST /v1/execrecipe and inputsCompiles a miss in the sandbox. See below.
GET /v1/statsnoneThe box's counters. See below.

Who sees what

  • lookup, pack and action answer with the public result of a key, else the caller's own claim, and nothing else. Entries with no such result are left out.
  • A blob is served to a principal that sent those bytes itself, or that asks through a result it may see (an output or the recipe of that result). A digest alone is not enough.
  • A blob the caller may not read is answered like one the box lacks: no frame, 404, or "missing". So no route says whether someone else's blob is there.

Blob asks

A blob ask in JSON is either a bare digest (a blob the caller sent itself) or an object {"key": <action key>, "blobs": [<digests>]} (blobs that result names). One request may mix both.

Frames

blobs/fetch answers, and blobs/put sends, one stream of frames. Each frame is the blob's 32-byte digest, its length as 8 bytes little-endian, then its bytes. Blobs the box lacks have no frame.

  • Send Accept-Encoding: zstd to get the same frames as one zstd stream (Content-Encoding: zstd).
  • blobs/put takes Content-Encoding: zstd too. Any other encoding gets 415.
  • The box keeps a frame only when its bytes have the digest it names. A bad frame costs only itself; the rest of the request goes on.

Packs

GET /v1/pack/{build_id} answers with an ETag. Send it back as If-None-Match and an unchanged pack is answered 304 with no body, so a rebuild after an edit costs one small request. Every pack answer also carries the X-Shippy-Exec header (below).

Remote execution: POST /v1/exec

The client sends Accept: application/x-shippy-exec and a body of frames: the first holds the request as JSON, the others are inputs the client expects the box to lack. Before anything runs, the box answers with a status:

StatusMeaningWhat the client does
200The request has a place in the queue. The body is a stream of records until the run ends.Reads the result and output blobs from the stream.
424Declared inputs are not in the store (or not readable by this caller). The body lists them in the recipe's order, without saying which is which.Sends them and asks again, at most twice.
429The caller has as many requests running and waiting as it may, or the queue is full. Has Retry-After.Compiles here, and asks the box to compile nothing for Retry-After seconds.
413The request is larger than the box takes.Asks once more without inputs, else compiles here.
400The request is not one the box can read.Asks once more uncompressed, else compiles here.
507The inputs would take this principal past its share of the store.Compiles here.
404 or 415A box from before this route.Compiles here for an hour.

The stream's records are a heartbeat (every five seconds), events as JSON, and the pieces of each output blob. Without the Accept header the route answers the result as JSON, or 422 with the reason. A declared input that is not in the store, or not readable by this caller, is such a reason ("missing input"), given before the request waits for a slot.

Every answer carries X-Shippy-Exec, a comma-separated list of what the box can do beyond the first version of the route: zstd (the request body may be one zstd stream) and finds (a request that declares a whole package may be answered from a result the box already has). See remote execution.

Worker routes

For worker and trusted-worker tokens only. Any other token gets 401. A worker reads no blob by digest, looks up no key and uploads nothing through the client routes.

Method and pathWhat it does
POST /v1/worker/pollAsks for a lease: a sandbox run to do.
POST /v1/worker/inputs/{lease}Fetches the inputs of a lease it holds.
POST /v1/worker/heartbeat/{lease}Says the lease is alive.
POST /v1/worker/early/{lease}Sends the crate's metadata ahead of the result.
POST /v1/worker/complete/{lease}Hands back the result.

A lease the worker lost or that ended gets 410. A completion the box refuses (outputs the recipe does not allow, bytes that are not their digest) gets 422 and the run goes to another worker. See workers.

Probes and metrics

Method and pathTokenAnswer
GET /healthznoneok: the process answers.
GET /readyznoneready, or 503 with not ready: <reasons>.
GET /metricsany role but a worker'sPrometheus text format.

/readyz is ready when the store's directories are there, the tokens file can be used, the last sandbox self-test passed and the box is not shutting down. The self-test compiles a crate at start and every five minutes (lifecycle.selftest_every). A box with exec.local = false has no sandbox to test and counts as passed.

metrics.listen (such as 127.0.0.1:9447) serves /metrics, /healthz and /readyz on a second address without a token. Keep it on loopback.

curl -fsS http://box:7447/healthz
curl -fsS http://box:7447/readyz
curl -fsS -H "Authorization: Bearer $TOKEN" http://box:7447/v1/stats | jq .

GET /v1/stats

One JSON object.

FieldWhat it counts
blobs, blob_bytesBlobs in the store and their size.
actions_trusted, actions_verified, actions_claimedResults by trust: uploaded by a trusted principal, produced by the box's sandbox, and claims.
verify_queueClaims waiting for a re-run.
verified_matchClaims whose bytes equalled the sandbox's.
verified_equivalentClaims whose rlibs differed only in archive member names.
verified_mismatchClaims whose content differed from the sandbox's.
verified_incrementalClaims that differed and whose recipe names incremental state (not in verified_mismatch).
verify_failedRe-runs that failed.
trusted_divergenceTrusted uploads whose content differed from the result already published for the key.
claims_not_verifiableClaims of a kind the box never re-runs.
lookups_hit, lookups_missLookups that found a result, and ones that did not.
evicted_results, evicted_claims, evicted_packs, evicted_blobs, evicted_bytesWhat eviction removed since the box started.
store_bytes, store_limitThe store's size and limit at the last eviction round.
sandboxHow runs are confined: limits (cgroup or rlimit), seccomp, and once known image and host_tools.
execRemote execution counters, below.
workersThe worker fleet, present only once leasing matters.

Under exec:

FieldWhat it counts
exec_slots, exec_running, exec_waitingSlots, and requests running and waiting now.
exec_startedRequests that got a slot.
exec_answeredRequests answered from a result that was already public.
exec_refusedRequests refused with 429.
exec_too_largeRequests refused for their size (413).
exec_missing_inputsRequests answered 424.
exec_earlyRuns whose metadata was sent ahead of the result.
exec_compile_errors, exec_failedRuns where the compiler failed, and runs that failed for another reason.
exec_discovered, exec_narrowed, exec_widened, exec_foundFor requests that declared a whole package: passes that found what the compiler reads, requests compiled with exactly that, ones compiled again with everything, and ones answered without a run.

Status codes worth knowing

StatusWhereMeaning
304GET /v1/packThe pack has not changed since the ETag you sent.
400any routeA malformed digest, body or request. One plain-text line says what.
401any authenticated routeNo token, or one that is unknown, expired, revoked or of the wrong role.
404action, blob, packNo such result, blob or pack, or one the caller may not read. An old box also answers 404 for routes it lacks.
410worker routesThe lease is not this worker's any more.
413uploads, pack, execThe body is larger than the route takes.
415blobs/putA Content-Encoding other than none or zstd.
422exec (plain JSON), worker completeThe box would not accept the request or the result.
424execDeclared inputs are missing; the body lists them.
429exec, blob, blobs/fetch, uploadsThe box is busy for this caller: no room to wait, or as many answers open as streams as one principal may have. Wait for Retry-After seconds.
503blobs/fetch, blobs/put, /readyz256 blob streams already run, or 64 of one untrusted principal's, or the box is not ready.
507uploads, pack, execAn untrusted principal would pass its share of the store (store.principal_share). The next eviction round makes room out of its own oldest data.

Errors are a status and one line of plain text. Every answer carries x-request-id; send your own (8 to 64 letters, digits, - or _) to match a client's log line with the box's.

Go deeper