8.17.3. JSONRPC-03 — §6 Batches: Many Messages in One Wire

This tutorial covers JSON-RPC 2.0 §6 batch requests — sending and receiving multiple messages in a single wire payload. The semantics are subtle (empty array is an error, all-notifications batch yields nothing on the wire, per-entry errors are individual envelopes), but daslib/jsonrpc handles all the edge cases.

8.17.3.1. Mixed batch round-trip

A client batches two requests plus one notification. The server dispatches them. The response is an array of two entries: the notification produces no response and input order is preserved.

let entries <- [
    make_request("ping", "null", 1),
    make_notification("log", "\{\"msg\":\"hello\"}"),
    make_request("echo", "[42]", 2)
]
let wire = make_batch(entries)

let resp = jsonrpc::dispatch_line(wire, false) $(m, p) {
    if (m == "ping") return "\"pong\""
    if (m == "echo") return jsonrpc::compact_json_whitespace(p)
    return "\"unknown\""
}
// resp = [{"jsonrpc":"2.0","id":1,"result":"pong"},{"jsonrpc":"2.0","id":2,"result":[42]}]

8.17.3.2. Per-entry errors: continue-on-error

Malformed entries — missing method, bad id type, etc. — get individual error envelopes in their array slot. Valid entries still dispatch:

let wire = "[\{\"id\":1,\"method\":\"ping\"},\{\"id\":2},\{\"id\":3,\"method\":\"echo\",\"params\":[\"x\"]}]"
let resp = jsonrpc::dispatch_line(wire, false) $(m, p) { return "ok" }
// resp has three entries:
//   id=1 result
//   id=2 error -32600 (missing method)
//   id=3 result

8.17.3.3. Empty batch array

Per §6, an empty array [] is itself an invalid request. The server responds with a SINGLE error envelope, not an array:

let resp = jsonrpc::dispatch_line("[]", false) $(m, p) { return "" }
// resp = {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request: empty batch"}}

8.17.3.4. All-notifications batch

If every entry in a batch is a notification, the server processes them (dispatcher runs for each) but emits nothing on the wire — the response array would be empty, so per §6 the server returns nothing at all:

let wire = "[\{\"method\":\"a\"},\{\"method\":\"b\"}]"
let resp = jsonrpc::dispatch_line(wire, false) $(m, p) { return "ignored" }
// resp == ""

8.17.3.5. Manual batch handling

For custom error semantics (per-method INVALID_PARAMS, METHOD_NOT_FOUND, logging per entry, etc.), use parse_batch directly:

def handle_batch(wire : string) : string {
    var pb <- parse_batch(wire)
    var out = pb.framing_error
    for (req in pb.requests) {
        if (!empty(req.error_envelope)) {
            // per-entry error — req.error_envelope is ready to go on the wire
        } else {
            // req.method, req.id_str, req.params, req.params_json available
        }
    }
    // assemble the response array from the per-entry results into `out`
    free_batch(pb)
    return out
}

One parse tree backs the whole batch (pb.document), and every entry’s params is a view into it — an entry never owns a document of its own. Read the entries, then close the scope with free_batch: it releases the tree and the entry array together, so pb.requests is empty afterwards. The per-entry strings outlive it. dispatch_line owns that scope itself.

8.17.3.6. Running the tutorial

daslang.exe tutorials/jsonrpc/03_batch.das

Full source: tutorials/jsonrpc/03_batch.das

8.17.3.7. See also