Skip to content

Discovering protocols

Every server co-hosts the framework protocol vgi_rpc.Reflection.v1 beside its application protocols. A client asks it what is hosted, and for any one protocol's methods and schemas, through the connection it already holds — on every transport:

#include <vgi_rpc/client.h>

vgi_rpc::RpcClientOptions options;
options.protocol = "Calculator";
auto client = vgi_rpc::RpcClient::spawn({"python", "worker.py"}, options);

for (const vgi_rpc::HostedProtocol& p : client.list_protocols()) {
    std::cout << p.name << ' ' << p.version << ' ' << p.hash << '\n';
}
vgi_rpc::ServiceDescription desc = client.describe_protocol("Calculator");
for (const auto& [name, method] : desc.methods) {
    std::cout << name << ": " << method.method_type << '\n';
}
client.call_unary("add", params);  // the connection is still yours

The same two members exist on RpcClient (spawned, caller-owned streams, Unix, named pipe, TCP and raw Iroh), HttpClient (HTTP, HTTPS and httpi://) and HttpSessionView:

Member Returns Round trips
list_protocols() std::vector<HostedProtocol>, in the server's order 1
describe_protocol(name) ServiceDescription 2: list_protocols, then describe(name)
describe() the application protocol's ServiceDescription 2

The held connection

Both reuse the client's own connection and never close it. An RpcClient sends the reflection call over its own byte stream — the server routes each request by its vgi_rpc.protocol key, so the client's own RpcClientOptions::protocol does not matter — and, being single-call-at-a-time, cannot ask while a stream is open. An HttpClient or HttpSessionView posts to {prefix}/vgi_rpc.Reflection.v1/{method} with its own connection pool, prefix, credentials, retry policy, sticky session and response budget. Nothing new is opened, and there is no public way to address vgi_rpc.Reflection.v1 directly: the protocol's service definition stays internal.

HostedProtocol

Field Meaning
name The wire name, which is the routing key and carries the major version.
version The declared semver, or "" when the protocol declares none.
hash SHA-256 of the canonical description, as 64 lowercase hex characters.
deprecated Whether callers should migrate off it (default false).
deprecation_message What to migrate to (default "").
features Capability tokens the protocol announces (default empty).

The order is the server's: application protocols in registration order, the primary first, then the framework's own (vgi_rpc.Reflection.v1, then vgi_rpc.Identity.v1 where it is hosted). Equal hashes mean an identical wire surface in any port, so a client that cached a description under a hash can skip describe_protocol(). Use the listing to discover an optional protocol before calling it, rather than calling it and reading an error.

Errors

A name the server does not host is the server's own answer: an RpcException (RpcRemoteError over HTTP) whose error_kind() is "protocol_not_supported". describe_protocol() lists first precisely so that this stays distinct from the next case.

A server without reflection — a Python reference server built without enable_describe=True (its default), or any server older than reflection — answers "not hosted": protocol_not_supported, an unknown method (method_not_implemented), code UNIMPLEMENTED, or over HTTP a bare 404 from a server older than protocol-scoped routes. All three members throw ReflectionNotSupportedError for that answer and for nothing else. It is an RpcException on every transport, HTTP included, carrying the server's fields (exception_type(), what(), error_code(), error_kind(), error_details(), server_id(), request_id()), plus http_status() (0 on a byte stream). No listing is ever inferred, since only the caller knows which protocol it expected the server to speak, and the connection remains usable.

This port's server hosts reflection unconditionally; there is no option to turn it off.