Skip to content

Direct request/response

Direct request/response is the synchronous RPC piece of the WebSocket protocol — for operations where you need a structured response, possibly large or chunked, in response to a specific request rather than as part of a stream.

It lives on the /v1/ws/commands endpoint alongside the command-publish surface, but is conceptually distinct: commands are fire and continue with results arriving later; direct requests are send and wait with a chunked response.

When you need it

Most external integrations won't. The common use cases:

  • Pulling a large response that doesn't fit a single REST call — when the message is more naturally chunked.
  • Operations bound to a specific live telescope where you want the hub to route through the SkyNode WS connection rather than going through a separate gateway round trip.
  • Internal protocols between hub services where the bidirectional nature of the WebSocket is the cleanest transport.

If you're not sure, start with REST. Move to direct request/response only if REST doesn't fit.

Protocol shape

A direct request is one direct_request message in, followed by a stream of response messages out:

client                                server
  │                                      │
  │── direct_request ──────────────────▶ │
  │                                      │
  │ ◀──── direct_response_start ─────────│   metadata + total_size
  │ ◀──── direct_response_chunk ─────────│   first chunk
  │ ◀──── direct_response_chunk ─────────│   ...
  │ ◀──── direct_response_chunk ─────────│   last chunk
  │ ◀──── direct_response_end ───────────│   stream complete
  │                                      │

If the request fails partway through, the server sends a single direct_error instead of direct_response_end:

  │ ◀──── direct_response_start ─────────│
  │ ◀──── direct_response_chunk ─────────│
  │ ◀──── direct_error ──────────────────│   request failed

Message shapes

Request

{
  "kind": "direct_request",
  "request_id": "<client-chosen id>",
  "telescope_uid": "<uid of target>",
  "request_type": "<application-specific>",
  "payload": { /* request-specific */ }
}

The request_type is a string discriminator chosen by the application layer; the catalog isn't centrally enumerated — each request type is documented (or should be) where it's implemented.

Response start

{
  "kind": "direct_response_start",
  "request_id": "<the same id>",
  "content_type": "application/octet-stream | image/fits | …",
  "total_size": 12345,
  "metadata": { /* request-type-specific */ }
}

total_size lets the receiver allocate ahead. content_type signals how to interpret the chunks.

Response chunk

{
  "kind": "direct_response_chunk",
  "request_id": "<the same id>",
  "data_b64": "<base64-encoded chunk>"
}

Chunk size is server-decided. The receiver concatenates chunks in the order they arrive.

Response end

{
  "kind": "direct_response_end",
  "request_id": "<the same id>"
}

Indicates a successful end of stream. Reassemble the chunks into the final payload of declared content_type.

Error

{
  "kind": "direct_error",
  "request_id": "<the same id>",
  "code": "invalid | forbidden | not_found | conflict | internal",
  "message": "<human-readable>",
  "details": { /* */ }
}

If you receive a direct_error at any point, the request has failed — discard any chunks received so far.

Patterns

Reassembling a response

const chunks: Uint8Array[] = [];
let contentType = "";

socket.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  if (msg.kind === "direct_response_start" && msg.request_id === myId) {
    contentType = msg.content_type;
    chunks.length = 0;
  }
  if (msg.kind === "direct_response_chunk" && msg.request_id === myId) {
    chunks.push(Uint8Array.from(atob(msg.data_b64), c => c.charCodeAt(0)));
  }
  if (msg.kind === "direct_response_end" && msg.request_id === myId) {
    const total = chunks.reduce((sum, c) => sum + c.length, 0);
    const payload = new Uint8Array(total);
    let offset = 0;
    for (const c of chunks) { payload.set(c, offset); offset += c.length; }
    // Use `payload` with `contentType`
  }
  if (msg.kind === "direct_error" && msg.request_id === myId) {
    // Handle error
  }
};

Reference

The direct protocol message types live at packages/py/skynet-sdk/skynet_sdk/schemas/ws_protocol.py (DirectRequestMessage, DirectResponseStartMessage, DirectResponseChunkMessage, DirectResponseEndMessage, DirectErrorMessage).