Developer docs

Developer docs/Transports

gRPC

The same API over HTTP/2 with generated clients. Two RPCs, `Call` and `Events`, carrying the API's JSON as text.

On this page: Build and run · The service · The token · Errors · Try it with grpcurl · Clients · Why an envelope


Build and run

gRPC is behind the grpc feature. It is off by default because it brings tokio and hyper, which nothing else in the workspace needs. CI builds and tests it on every change.

cd src
cargo run -p concat-cli --features grpc -- serve --grpc 127.0.0.1:7421
Concat API 0.2: gRPC on 127.0.0.1:7421
Concat API 0.2: token 3f9c1e…

--grpc combines with --json and --socket; all callers share one dispatcher. A build without the feature refuses --grpc with a message saying so.


The service

src/crates/concat-server/proto/concat.proto, package concat.v1:

service Concat {
  rpc Call   (Request)       returns (Response);      // one call, one response
  rpc Events (EventsRequest) returns (stream Event);  // every job's events, while open
}

message Request  { string method = 1; string params = 2; }   // params: JSON text, or empty
message Response { oneof outcome { string result = 1; Error error = 2; } }
message Error    { string code = 1; int32 number = 2; string message = 3; }
message EventsRequest {}
message Event    { string event = 1; string params = 2; }    // params: JSON text
FieldHolds
Request.methodThe method name, e.g. "edit.apply"
Request.paramsThe params object as JSON text. Empty for a method with none
Response.resultThe reply as JSON text: exactly what a JSON-RPC result holds
Response.errorcode is the API’s name (notOpen, …), number its JSON-RPC number, message the sentence
Event.event"export.progress", …
Event.paramsIts fields as JSON text

Everything in Methods, Edit commands and Types applies unchanged. Parse result and params with your JSON library.


The token

Every call carries the server’s token as bearer metadata:

authorization: Bearer 3f9c1e…

Missing or wrong: the RPC fails with status UNAUTHENTICATED. This applies to Events too. The comparison is constant-time. Where the token comes from is in JSON-RPC → The token.


Errors

Two layers, deliberately kept apart:

LayerMeansLooks like
gRPC statusthe transport refusedUNAUTHENTICATED (token), INTERNAL (the server could not run the call at all)
Response.errorthe API refused{code: "notOpen", number: -32001, message: "…"} with the call itself returning OK

So a Call that returns OK still needs its outcome checked.


Try it with grpcurl

There is no reflection service; hand grpcurl the proto.

PROTO=src/crates/concat-server/proto/concat.proto
TOKEN=3f9c1e…

grpcurl -plaintext -proto $PROTO -H "authorization: Bearer $TOKEN" \
  -d '{"method":"version","params":""}' \
  127.0.0.1:7421 concat.v1.Concat/Call
{
  "result": "{\"apiVersion\":\"0.2\",\"concat\":\"0.2.4\",\"dirs\":{…},\"capabilities\":[\"events\",\"grpc\"]}"
}

An edit:

grpcurl -plaintext -proto $PROTO -H "authorization: Bearer $TOKEN" \
  -d '{"method":"edit.apply","params":"{\"path\":\"/edits/Reel\",\"command\":{\"op\":\"addTrack\"}}"}' \
  127.0.0.1:7421 concat.v1.Concat/Call

Events, streamed until you stop it:

grpcurl -plaintext -proto $PROTO -H "authorization: Bearer $TOKEN" \
  -d '{}' 127.0.0.1:7421 concat.v1.Concat/Events

Clients

Rust

The generated client ships in the crate under the feature:

use concat_server::grpc::proto::{concat_client::ConcatClient, Request as Call, EventsRequest};
use tonic::Request;

let mut client = ConcatClient::connect("http://127.0.0.1:7421").await?;
let mut request = Request::new(Call { method: "version".into(), params: String::new() });
request.metadata_mut().insert("authorization", format!("Bearer {token}").parse()?);
let response = client.call(request).await?.into_inner();

Python

pip install grpcio grpcio-tools
python -m grpc_tools.protoc -I src/crates/concat-server/proto \
  --python_out=. --grpc_python_out=. concat.proto
import json, grpc, concat_pb2, concat_pb2_grpc

channel = grpc.insecure_channel("127.0.0.1:7421")
stub = concat_pb2_grpc.ConcatStub(channel)
meta = [("authorization", f"Bearer {TOKEN}")]

def call(method, **params):
    req = concat_pb2.Request(method=method, params=json.dumps(params) if params else "")
    res = stub.Call(req, metadata=meta)
    if res.HasField("error"):
        raise RuntimeError(f"{res.error.code}: {res.error.message}")
    return json.loads(res.result)

print(call("version")["capabilities"])
for event in stub.Events(concat_pb2.EventsRequest(), metadata=meta):
    print(event.event, json.loads(event.params))

Anything else

Any language with a protobuf and gRPC toolchain: generate from concat.proto, send the bearer, parse the JSON text.


Why an envelope and not a second schema

The protobuf carries JSON text rather than typed messages on purpose:

  • A method added to the API reaches gRPC with no change to the proto.
  • There is one set of docs for every transport.
  • Both transports parse requests through the same code, so they refuse the same things the same way.

What gRPC adds over the line transport is HTTP/2, generated clients, and a streamed reply for events.

Edit this page on GitHub