Vix.cpp v2.8.5 is here Read the blog
Skip to content

Quick Start ​

The webrpc module gives Vix applications a small RPC core that is independent from the transport.

A WebRPC call starts as a JSON-like vix::json::token. The dispatcher parses the request envelope, sends the method to a router, executes the matching handler, and returns a response token when a response is expected.

The basic workflow is:

txt
create router
  -> register methods
  -> create dispatcher
  -> pass request token
  -> receive response token

Use the public WebRPC header:

cpp
#include <vix/webrpc/webrpc.hpp>

WebRPC uses the Vix JSON token model:

cpp
#include <vix/json/Simple.hpp>

For examples that print output:

cpp
#include <vix/print.hpp>

Create a router ​

A Router stores RPC method handlers.

cpp
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  vix::webrpc::Router router;

  vix::print("router created");

  return 0;
}

The router is transport-agnostic. It does not know whether the call came from HTTP, WebSocket, P2P, CLI, or a local adapter.

Register a method ​

Use router.add(...) to register a method name and a handler.

cpp
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  using namespace vix::json;
  using namespace vix::webrpc;

  Router router;

  router.add("ping", [](const Context &) -> RpcResult
  {
    return obj({
        "pong", true,
    });
  });

  vix::print("registered methods:", router.size());

  return 0;
}

A handler receives a Context and returns RpcResult.

cpp
using RpcResult =
    std::variant<vix::json::token, vix::webrpc::RpcError>;

Returning a vix::json::token means success. Returning RpcError means failure.

Dispatch a request ​

Create a request token and pass it to Dispatcher.

cpp
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  using namespace vix::json;
  using namespace vix::webrpc;

  Router router;

  router.add("ping", [](const Context &) -> RpcResult
  {
    return obj({
        "pong", true,
    });
  });

  token request = obj({
      "id", 1LL,
      "method", "ping",
  });

  Dispatcher dispatcher(router);

  auto response = dispatcher.handle(request, "local");

  if (!response.has_value())
  {
    vix::print("no response");
    return 0;
  }

  vix::print("response created");

  return 0;
}

The dispatcher returns std::optional<vix::json::token>. A normal request with an id produces a response. A notification without an id produces no response.

Inspect the response ​

A successful response contains id and result.

cpp
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  using namespace vix::json;
  using namespace vix::webrpc;

  Router router;

  router.add("ping", [](const Context &) -> RpcResult
  {
    return obj({
        "pong", true,
    });
  });

  token request = obj({
      "id", 1LL,
      "method", "ping",
  });

  Dispatcher dispatcher(router);

  auto response = dispatcher.handle(request, "local");
  if (!response.has_value())
  {
    return 1;
  }

  const auto *response_object = response->as_object_ptr().get();
  if (!response_object)
  {
    return 1;
  }

  const token *result = response_object->get_ptr("result");
  if (!result)
  {
    return 1;
  }

  const auto *result_object = result->as_object_ptr().get();
  if (!result_object)
  {
    return 1;
  }

  vix::print("pong:", result_object->get_bool_or("pong", false));

  return 0;
}

In most real applications, a transport adapter serializes the response token back to the client. Local tests and examples can inspect the token directly.

Read parameters ​

Handlers read input through Context::params.

cpp
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  using namespace vix::json;
  using namespace vix::webrpc;

  Router router;

  router.add("math.add", [](const Context &ctx) -> RpcResult
  {
    const auto *params = ctx.params_object_ptr();
    if (!params)
    {
      return RpcError::invalid_params("params must be an object");
    }

    const token *a = params->get_ptr("a");
    const token *b = params->get_ptr("b");

    if (!a || !b || !a->is_i64() || !b->is_i64())
    {
      return RpcError::invalid_params("a and b must be integers");
    }

    const auto sum = a->as_i64_or(0) + b->as_i64_or(0);

    return obj({
        "sum", sum,
    });
  });

  token request = obj({
      "id", 1LL,
      "method", "math.add",
      "params", obj({
          "a", 7LL,
          "b", 5LL,
      }),
  });

  Dispatcher dispatcher(router);

  auto response = dispatcher.handle(request, "local");

  if (response.has_value())
  {
    vix::print("math.add response created");
  }

  return 0;
}

Handlers are responsible for validating the shape and type of their own parameters. If parameters are invalid, return RpcError::invalid_params(...).

Return an error ​

A handler can return a structured RpcError.

cpp
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  using namespace vix::json;
  using namespace vix::webrpc;

  Router router;

  router.add("user.get", [](const Context &ctx) -> RpcResult
  {
    const auto *params = ctx.params_object_ptr();
    if (!params)
    {
      return RpcError::invalid_params("params must be an object");
    }

    const token *id = params->get_ptr("id");
    if (!id || !id->is_i64())
    {
      return RpcError::invalid_params("id must be an integer");
    }

    return obj({
        "id", id->as_i64_or(0),
        "name", "Gaspard",
    });
  });

  token request = obj({
      "id", 1LL,
      "method", "user.get",
      "params", obj({
          "id", "bad",
      }),
  });

  Dispatcher dispatcher(router);

  auto response = dispatcher.handle(request, "local");

  if (response.has_value())
  {
    vix::print("error response created");
  }

  return 0;
}

The dispatcher wraps the returned error into a response envelope:

json
{
  "id": 1,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "Invalid RPC parameters",
    "details": {
      "reason": "id must be an integer"
    }
  }
}

Handle missing methods ​

If a request calls a method that is not registered, the router returns METHOD_NOT_FOUND.

cpp
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  using namespace vix::json;
  using namespace vix::webrpc;

  Router router;
  Dispatcher dispatcher(router);

  token request = obj({
      "id", 1LL,
      "method", "missing.method",
  });

  auto response = dispatcher.handle(request, "local");

  if (response.has_value())
  {
    vix::print("method-not-found response created");
  }

  return 0;
}

The error response keeps the failure explicit and machine-readable.

Use notifications ​

A request without an id is a notification.

cpp
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  using namespace vix::json;
  using namespace vix::webrpc;

  Router router;

  router.add("events.track", [](const Context &ctx) -> RpcResult
  {
    const auto *params = ctx.params_object_ptr();
    if (!params)
    {
      return RpcError::invalid_params("params must be an object");
    }

    vix::print("event received");

    return obj({
        "ok", true,
    });
  });

  token notification = obj({
      "method", "events.track",
      "params", obj({
          "name", "page.open",
      }),
  });

  Dispatcher dispatcher(router);

  auto response = dispatcher.handle(notification, "local");

  if (!response.has_value())
  {
    vix::print("notification handled without response");
  }

  return 0;
}

The handler is still executed, but the dispatcher returns no response. This is useful for fire-and-forget events.

Use batch calls ​

The dispatcher can handle an array of request objects.

cpp
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  using namespace vix::json;
  using namespace vix::webrpc;

  Router router;

  router.add("ping", [](const Context &) -> RpcResult
  {
    return obj({
        "pong", true,
    });
  });

  token batch = array_t{
      {
          obj({
              "id", 1LL,
              "method", "ping",
          }),
          obj({
              "id", 2LL,
              "method", "ping",
          }),
      },
  };

  Dispatcher dispatcher(router);

  auto response = dispatcher.handle(batch, "local");

  if (response.has_value())
  {
    vix::print("batch response created");
  }

  return 0;
}

A batch response is an array of response objects. Notifications inside a batch are executed, but they are not included in the response array.

Pass metadata ​

Transport adapters can pass metadata to handlers through the context.

cpp
#include <string>
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  using namespace vix::json;
  using namespace vix::webrpc;

  Router router;

  router.add("whoami", [](const Context &ctx) -> RpcResult
  {
    const auto peer = ctx.meta_value("peer");

    return obj({
        "peer", std::string(peer),
        "transport", std::string(ctx.transport),
    });
  });

  Context::MetaMap meta{
      {"peer", "client-1"},
      {"trace_id", "abc-123"},
  };

  token request = obj({
      "id", 1LL,
      "method", "whoami",
  });

  Dispatcher dispatcher(router);

  auto response = dispatcher.handle(request, "websocket", &meta);

  if (response.has_value())
  {
    vix::print("metadata response created");
  }

  return 0;
}

Metadata is optional. It is useful for headers, peer ids, trace ids, authentication context, or transport-specific tags.

Direct router dispatch ​

Use Router::dispatch(...) directly when you already have a parsed request and do not need response wrapping.

cpp
#include <variant>
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  using namespace vix::json;
  using namespace vix::webrpc;

  Router router;

  router.add("echo", [](const Context &ctx) -> RpcResult
  {
    return ctx.params;
  });

  RpcRequest request{
      token(1LL),
      "echo",
      obj({
          "message", "hello",
      }),
  };

  RpcResult result = router.dispatch(request, "local");

  if (std::holds_alternative<RpcError>(result))
  {
    const auto &error = std::get<RpcError>(result);
    vix::print("error:", error.code);
    return 1;
  }

  vix::print("router dispatch succeeded");

  return 0;
}

Use Router directly for internal calls, tests, or custom dispatch flows. Use Dispatcher when you want request parsing, batch handling, notifications, and response envelopes.

Complete example ​

cpp
#include <string>
#include <vix/webrpc/webrpc.hpp>
#include <vix/json/Simple.hpp>
#include <vix/print.hpp>

int main()
{
  using namespace vix::json;
  using namespace vix::webrpc;

  Router router;

  router.add("math.add", [](const Context &ctx) -> RpcResult
  {
    const auto *params = ctx.params_object_ptr();
    if (!params)
    {
      return RpcError::invalid_params("params must be an object");
    }

    const token *a = params->get_ptr("a");
    const token *b = params->get_ptr("b");

    if (!a || !b || !a->is_i64() || !b->is_i64())
    {
      return RpcError::invalid_params("a and b must be integers");
    }

    const auto sum = a->as_i64_or(0) + b->as_i64_or(0);

    return obj({
        "sum", sum,
    });
  });

  router.add("ping", [](const Context &) -> RpcResult
  {
    return obj({
        "pong", true,
    });
  });

  Dispatcher dispatcher(router);

  token request = obj({
      "id", 1LL,
      "method", "math.add",
      "params", obj({
          "a", 7LL,
          "b", 5LL,
      }),
  });

  auto response = dispatcher.handle(request, "local");

  if (!response.has_value())
  {
    vix::print("no response");
    return 0;
  }

  const auto *response_object = response->as_object_ptr().get();
  if (!response_object)
  {
    vix::print("invalid response");
    return 1;
  }

  if (const token *error = response_object->get_ptr("error"))
  {
    const auto parsed = RpcError::parse(*error);

    if (parsed.ok())
    {
      vix::print("rpc error:", parsed.value().code);
    }

    return 1;
  }

  const token *result = response_object->get_ptr("result");
  if (!result)
  {
    vix::print("missing result");
    return 1;
  }

  const auto *result_object = result->as_object_ptr().get();
  if (!result_object)
  {
    vix::print("invalid result");
    return 1;
  }

  vix::print("sum:", result_object->get_i64_or("sum", 0));

  return 0;
}

This is the normal WebRPC path: register handlers, dispatch a token, and inspect the response.

What to remember ​

WebRPC does not replace a transport layer. It gives the transport layer a stable RPC core.

An HTTP adapter, WebSocket adapter, P2P adapter, or CLI adapter can all use the same router and dispatcher. The adapter only needs to convert incoming data into a vix::json::token, call the dispatcher, and send the response token back when one exists.

Next step ​

Continue with requests to understand the request envelope, parsing rules, request ids, parameters, and notification behavior.

Released under the MIT License.