ndjson-rpc 0.0.1

Line-delimited JSON-RPC framing with Unix socket and HTTP/1.1 chunked transports, supporting one-shot and streaming responses.
Documentation

Line-delimited JSON RPC

A small RPC framing crate that speaks { id, verb, args }{ id, result | error | event | end } newline-delimited JSON, with two interchangeable transports: a Unix-domain-socket server / client for local control planes, and an HTTP/1.1 server / client that streams the same frames as Transfer-Encoding: chunked NDJSON.

Features

Daemon ↔ CLI control planes (containerd, caddy, kubelet, …) are a recurring pattern that everyone re-implements. tower / jsonrpsee solve a much bigger problem and pull in a lot of weight; this crate stays in the "framed bytes + dispatch trait" lane and is small enough to fork when you need to.

Server

Implement [Handler] against your application state, hand an Arc<H> to either [spawn_unix_server] or [spawn_http_server] (or both):

use std::sync::Arc;
use async_trait::async_trait;
use ndjson_rpc::{DispatchOutcome, Handler, Request, WireError, WireErrorKind, spawn_unix_server};
use tokio_util::sync::CancellationToken;

struct App;

#[async_trait]
impl Handler for App {
    async fn dispatch(&self, req: Request) -> DispatchOutcome {
        match req.verb.as_str() {
            "ping" => DispatchOutcome::OneShot(Ok(serde_json::json!({ "pong": true }))),
            other => DispatchOutcome::OneShot(Err(WireError {
                kind: WireErrorKind::UnknownVerb,
                message: format!("unknown verb: {other}"),
            })),
        }
    }
}

# async fn run() -> std::io::Result<()> {
let cancel = CancellationToken::new();
let _task = spawn_unix_server(
    std::path::Path::new("/run/myapp.sock"),
    Arc::new(App),
    cancel,
).await?;
# Ok(())
# }

Streaming verbs return DispatchOutcome::Stream(Box<dyn EventStream + Send>); the server frames each Some(value) as an Event and writes a final End when the stream returns None. The client cancels by closing the socket — the EventStream is dropped, and any cleanup it owns runs through Drop.

Client

[UnixMgmtClient] (Unix socket) and [HttpMgmtClient] (HTTP/1.1 + optional bearer token) both expose call(verb, args) for one-shot verbs and call_stream(verb, args, on_event) / stream(...) for streaming verbs. Each call opens a fresh connection — the protocol is verb-at-a-time and not chatty enough to amortize a connection pool.

License

Released under the MIT License © 2026 Canmi