src/fl/stl/asio/http/PROTOCOL.md
This document specifies the protocol for JSON-RPC 2.0 communication over HTTP/1.1 with chunked transfer encoding. The protocol supports three RPC modes (SYNC, ASYNC, ASYNC_STREAM) with long-lived, bidirectional streaming connections.
All RPC requests are sent as HTTP POST to the /rpc endpoint:
POST /rpc HTTP/1.1
Host: <server-address>
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
<chunk-size-hex>\r\n
<json-rpc-request>\r\n
<chunk-size-hex>\r\n
<json-rpc-request>\r\n
...
0\r\n
\r\n
| Header | Value | Required | Description |
|---|---|---|---|
Content-Type | application/json | Yes | JSON-RPC payload |
Transfer-Encoding | chunked | Yes | Enables streaming |
Connection | keep-alive | Yes | Persistent connection |
Host | <server:port> | Yes | Server address |
Follows JSON-RPC 2.0 specification:
{
"jsonrpc": "2.0",
"method": "<method-name>",
"params": <params-array-or-object>,
"id": <request-id>
}
Fields:
jsonrpc (string, required): Must be "2.0"method (string, required): RPC method nameparams (array/object, optional): Method parametersid (number/string/null, required): Request identifier
null for notifications (no response expected)All RPC responses are sent over a single long-lived HTTP response with chunked encoding:
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
<chunk-size-hex>\r\n
<json-rpc-response>\r\n
<chunk-size-hex>\r\n
<json-rpc-response>\r\n
...
0\r\n
\r\n
| Header | Value | Required | Description |
|---|---|---|---|
Content-Type | application/json | Yes | JSON-RPC payload |
Transfer-Encoding | chunked | Yes | Enables streaming |
Connection | keep-alive | Yes | Persistent connection |
{
"jsonrpc": "2.0",
"result": <result-value>,
"id": <request-id>
}
{
"jsonrpc": "2.0",
"error": {
"code": <error-code>,
"message": "<error-message>",
"data": <optional-error-data>
},
"id": <request-id>
}
Standard Error Codes (JSON-RPC 2.0):
-32700: Parse error (invalid JSON)-32600: Invalid request (invalid JSON-RPC structure)-32601: Method not found-32602: Invalid params-32603: Internal error-32000 to -32099: Server-defined errorsBehavior: Server processes request and returns result immediately.
Request Example:
POST /rpc HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
2F\r\n
{"jsonrpc":"2.0","method":"add","params":[1,2],"id":1}\r\n
0\r\n
\r\n
Response Example:
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
27\r\n
{"jsonrpc":"2.0","result":3,"id":1}\r\n
0\r\n
\r\n
Flow Diagram:
Client Server
| |
|---Request---------->|
| | Process
|<--Result------------|
| |
Behavior: Server sends immediate ACK, then processes request and sends result later.
Request Example:
POST /rpc HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
36\r\n
{"jsonrpc":"2.0","method":"longTask","params":{},"id":2}\r\n
0\r\n
\r\n
Response Example:
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
30\r\n
{"jsonrpc":"2.0","result":{"ack":true},"id":2}\r\n
2D\r\n
{"jsonrpc":"2.0","result":{"value":42},"id":2}\r\n
0\r\n
\r\n
ACK Object:
{"ack": true}
Result Object:
{"value": <final-result>}
Flow Diagram:
Client Server
| |
|---Request---------->|
|<--ACK---------------|
| | Process (async)
|<--Result------------|
| |
Behavior: Server sends immediate ACK, then streams multiple updates, and finally sends result with stop marker.
Request Example:
POST /rpc HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
39\r\n
{"jsonrpc":"2.0","method":"streamData","params":{},"id":3}\r\n
0\r\n
\r\n
Response Example:
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
30\r\n
{"jsonrpc":"2.0","result":{"ack":true},"id":3}\r\n
30\r\n
{"jsonrpc":"2.0","result":{"update":10},"id":3}\r\n
30\r\n
{"jsonrpc":"2.0","result":{"update":20},"id":3}\r\n
30\r\n
{"jsonrpc":"2.0","result":{"update":30},"id":3}\r\n
3C\r\n
{"jsonrpc":"2.0","result":{"value":100,"stop":true},"id":3}\r\n
0\r\n
\r\n
ACK Object:
{"ack": true}
Update Object (sent 0+ times):
{"update": <progress-value>}
Final Object (sent exactly once):
{"value": <final-result>, "stop": true}
Flow Diagram:
Client Server
| |
|---Request---------->|
|<--ACK---------------|
| | Process (streaming)
|<--Update------------|
|<--Update------------|
|<--Update------------|
|<--Final+Stop--------|
| |
IMPORTANT: The stop marker MUST be present in the final response to indicate end of stream.
To prevent connection timeouts and detect dead connections, both client and server send periodic heartbeat messages.
Sent by either party to check connection liveness:
{"jsonrpc":"2.0","method":"rpc.ping","id":null}
Notes:
id is null (notification, no response required if unimplemented)rpc.ping is reserved for heartbeatOptional response to ping (if implemented):
{"jsonrpc":"2.0","result":"pong","id":null}
Notes:
rpc.ping, it SHOULD respond with "pong"rpc.ping, it MAY ignore the notificationDefault Configuration:
Behavior:
Client Server
| |
|---TCP Connect------>|
|<--TCP Accept--------|
| |
|---HTTP POST /rpc--->|
|<--HTTP 200 OK-------|
| |
| (connection established, streaming begins)
Client Server
|<====Requests=======>|
|<====Responses=======|
|<====Heartbeats======|
| (bidirectional streaming)
Trigger: Connection lost (timeout, network error, server restart)
Exponential Backoff:
Flow:
Client Server
| |
| (connection lost) X
| |
| (wait 1s) |
|---Reconnect-------->|
| (failed) X
| |
| (wait 2s) |
|---Reconnect-------->|
|<--Connected---------|
| |
| (streaming resumes) |
Max Reconnection Attempts: Configurable (default: unlimited)
Scenarios:
Client Behavior:
onDisconnect() callbackNon-200 Status Codes:
4xx (Client Error): Log and report to user5xx (Server Error): Log and attempt reconnectionMalformed HTTP:
Client Behavior:
Parse Errors (-32700):
{
"jsonrpc": "2.0",
"error": {
"code": -32700,
"message": "Parse error",
"data": "Invalid JSON at position 42"
},
"id": null
}
Method Not Found (-32601):
{
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": "Method not found",
"data": "add"
},
"id": 1
}
Invalid Params (-32602):
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params",
"data": "Expected 2 parameters, got 1"
},
"id": 1
}
Production Deployments SHOULD:
This Specification:
Not Specified: This protocol does not define authentication mechanisms.
Recommendations:
Not Specified: This protocol does not define rate limiting.
Recommendations:
Retry-After header with backoff timeMUST:
/rpc endpointContent-Type, Transfer-Encoding, Connection)SHOULD:
onConnect, onDisconnect)MAY:
MUST:
/rpc endpointSHOULD:
MAY:
rpc.ping method (respond to heartbeat)Client Code (pseudocode):
// Connect
auto client = HttpStreamClient("localhost", 8080);
client.connect();
// Send SYNC request
Json request = Json::object()
.set("jsonrpc", "2.0")
.set("method", "add")
.set("params", Json::array().add(1).add(2))
.set("id", 1);
client.writeRequest(request);
// Receive response
auto response = client.readResponse();
// response = {"jsonrpc":"2.0","result":3,"id":1}
Server Code (pseudocode):
// Bind method
remote.bind("add", [](int a, int b) { return a + b; });
// Accept connection
auto server = HttpStreamServer(8080);
server.listen();
// Handle request
auto request = server.readRequest();
auto response = remote.handle(request);
server.writeResponse(response);
Client Code (pseudocode):
// Send ASYNC request
Json request = Json::object()
.set("jsonrpc", "2.0")
.set("method", "longTask")
.set("params", Json::object())
.set("id", 2);
client.writeRequest(request);
// Receive ACK
auto ack = client.readResponse();
// ack = {"jsonrpc":"2.0","result":{"ack":true},"id":2}
// Receive result (later)
auto result = client.readResponse();
// result = {"jsonrpc":"2.0","result":{"value":42},"id":2}
Server Code (pseudocode):
// Bind ASYNC method
remote.bindAsync("longTask", [](ResponseSend& send, const Json& params) {
// Send ACK immediately
send.send(Json::object().set("ack", true));
// Do work asynchronously
std::thread([&send]() {
sleep(5); // Simulate long task
send.send(Json::object().set("value", 42));
}).detach();
}, RpcMode::ASYNC);
Client Code (pseudocode):
// Send ASYNC_STREAM request
Json request = Json::object()
.set("jsonrpc", "2.0")
.set("method", "streamData")
.set("params", Json::object())
.set("id", 3);
client.writeRequest(request);
// Receive ACK
auto ack = client.readResponse();
// ack = {"jsonrpc":"2.0","result":{"ack":true},"id":3}
// Receive updates
while (true) {
auto update = client.readResponse();
if (update["result"]["stop"].as_bool()) {
// Final result
// update = {"jsonrpc":"2.0","result":{"value":100,"stop":true},"id":3}
break;
} else {
// Progress update
// update = {"jsonrpc":"2.0","result":{"update":10},"id":3}
}
}
Server Code (pseudocode):
// Bind ASYNC_STREAM method
remote.bindAsync("streamData", [](ResponseSend& send, const Json& params) {
// Send ACK immediately
send.send(Json::object().set("ack", true));
// Stream updates
std::thread([&send]() {
for (int i = 0; i < 10; i++) {
send.sendUpdate(Json::object().set("update", i * 10));
sleep(1);
}
send.sendFinal(Json::object().set("value", 100));
}).detach();
}, RpcMode::ASYNC_STREAM);
Multiple requests in single HTTP POST:
Request:
[
{"jsonrpc":"2.0","method":"add","params":[1,2],"id":1},
{"jsonrpc":"2.0","method":"subtract","params":[5,3],"id":2}
]
Response:
[
{"jsonrpc":"2.0","result":3,"id":1},
{"jsonrpc":"2.0","result":2,"id":2}
]
Request with id: null:
Request:
{"jsonrpc":"2.0","method":"log","params":["Hello"],"id":null}
Response: None (notification, no response expected)
jsonrpc: "2.0" field in all requests/responsesmethod field in requestsparams field (array or object)id field (number, string, or null)result field in success responseserror field in error responsesid: null)