docs: rewrite JsonRpcBundle docs, add SDK docs, move them under docs/api
en/docs/api/json-rpc-bundle/developers/lifecycle.md
new file +95
ā0
1
+
# Request lifecycle
2
+
3
+
How JsonRpcBundle processes a request from the moment it is received until the response ā for each entry point. Useful for writing [event](/docs/api/json-rpc-bundle/functionality/events) listeners and [extensions](/docs/api/json-rpc-bundle/developers).
4
+
5
+
## HTTP POST
6
+
7
+
The bundle's routes are declared as `GET` (for documentation); POST requests to the same address are intercepted on `kernel.request` before routing and the controller.
8
+
9
+
1. **`kernel.request`, priority 240 ā `SymfonyFlowListener::initHttpRequest`**
10
+
* determines the [API group](/docs/api/json-rpc-bundle/functionality/groups) from the route and writes it to `ApiGroupContext`;
11
+
* `OPTIONS` (CORS preflight) gets an empty response;
12
+
* for POST, creates the `RpcFromHttp` carrier (JSON parsing; a body starting with `[` is a batch), the `HttpTokenHolder` token holder, and checks the token (if `protected_api: true`) ā with the group's validator or the base one.
13
+
2. **`kernel.request`, priority 230 ā `RpcRequestHandler::handle()`**
14
+
* **batch** ā checks `batch.enabled` and for an empty batch, selects a strategy (`IBatchStrategy`) and executes the items;
15
+
* **single request** ā checks `jsonrpc: "2.0"`, fires the **`rpc.request`** event, then one of the branches:
16
+
* request with `$rpc.callback` ā an immediate placeholder response and **`rpc.pre_response`**, execution is deferred;
17
+
* notification ā a placeholder and **`rpc.pre_response`**, execution is deferred;
18
+
* regular request ā `RpcServer::handle()`.
19
+
3. **`RpcServer::handle()`**
20
+
* looks up the method in the service map (taking the group and version into account); an unknown method is error `-32601`;
21
+
* **cache**: for methods with `#[RPC\Cache]` (and without `$rpc.ignoreCache`) a ready response is returned immediately, without execution events;
22
+
* the **`rpc.pre_execute`** event, during which, in order: argument building (1002), validation (1001), conversion (1000), `#[RPC\Lock]` (-100), **method call** (-1000);
23
+
* success ā **`rpc.pre_response`**, then **`rpc.post_execute`**; error ā **`rpc.error`**, then **`rpc.pre_response`**;
24
+
* storing the response in the cache (for `#[RPC\Cache]`).
25
+
4. **Response** ā JSON; `204` with no body if there is nothing to respond with (notification, a batch of notifications only). The Symfony router and controller do not run for POST.
26
+
5. **`kernel.terminate`** ā after `fastcgi_finish_request()` (PHP-FPM), **`rpc.post_response`** is fired for each successfully executed request: invalidation of `#[RPC\CacheRelation]` and execution of notifications / callback. Then `ApiGroupContext` is reset.
27
+
28
+
Errors that occur before the method is executed (invalid JSON, token, unknown method, etc.) are turned into a response by `SymfonyErrorListener` on `kernel.exception`. HTTP status: 401 ā no token passed, 403 ā invalid token, 500 ā invalid JSON, otherwise 200.
29
+
30
+
## HTTP GET (documentation)
31
+
32
+
1. Priority 240 ā group detection; if `protected_doc: true` ā token check.
33
+
2. Priority 8 ā language selection from `Accept-Language` (`RpcLocaleNegotiator`).
34
+
3. Priority 4 ā outside debug mode, a response from the cache: `304` for `If-None-Match` or the stored document.
35
+
4. `ApiController::openrpc()` / `postman()` builds the document for the version and group.
36
+
5. `kernel.response` ā `Content-Language`, `Vary: Accept-Language`, `ETag`, storing in the cache (only for `200`).
37
+
38
+
No RPC events are fired on this path.
39
+
40
+
## Console: `ufo:rpc:process`
41
+
42
+
The command has no logic of its own ā the request is handled by a `console.command` listener (priority 999999):
43
+
44
+
1. reading JSON (argument, stdin `-` or the `UFO_RPC_REQUEST` variable);
45
+
2. group from `--api-group`, version from `--ver`;
46
+
3. the `RpcFromCli` carrier, token from the `--<token_name>` option or the `UFO_RPC_TOKEN` variable;
47
+
4. `RpcRequestHandler::handle()` ā then as in HTTP;
48
+
5. JSON output to stdout;
49
+
6. `console.terminate` ā `rpc.post_response`.
50
+
51
+
Errors are handled on `console.error` and go through `rpc.error`.
52
+
53
+
## Symfony Messenger
54
+
55
+
`RpcAsyncProcessor` is the handler for `RpcAsyncRequest` messages:
56
+
57
+
1. group from `$message->apiGroup`, token from `$message->token`, token check;
58
+
2. direct call of `RpcServer::handle()`.
59
+
60
+
So for requests from the queue there is **no** `rpc.request` and `rpc.post_response`; `rpc.pre_execute`, `rpc.pre_response`, `rpc.post_execute` and `rpc.error` are fired. Token and unknown method errors become Messenger message handling errors (retries, failure transport).
61
+
62
+
## Batch
63
+
64
+
* **Process mode** (`batch.process: true`) ā each ready item is run in a child process `ufo:rpc:process -` (request via stdin, token via an environment variable, group and version as options), at most `batch.process_count` at a time. Each child process is a full console cycle with its own events. The parent process fires **`rpc.async_output`** on the output of each; when a `@FROM` dependency is resolved, it starts the next items.
65
+
* **Inline mode** (`batch.process: false`) ā items are executed one by one in the current process via `RpcExecutor`, each with its own event pool; `rpc.post_response` ā right after the item.
66
+
* Items whose dependency failed end with an error in the parent process.
67
+
68
+
## Notifications and `$rpc.callback`
69
+
70
+
1. On receipt ā `rpc.request` and `rpc.pre_response` with a placeholder; the client gets `204` (notification) or `{"async": true, "callback": ā¦}`.
71
+
2. On `rpc.post_response` (priority 10), `AsyncServerEventListener` executes the method ā the full cycle `rpc.pre_execute` ā `rpc.pre_response` ā `rpc.post_execute` / `rpc.error`.
72
+
3. For a callback, the result is sent as a POST request to the specified URL.
73
+
74
+
With `notification.process: true`, a single HTTP request is handed off to a detached `ufo:rpc:process @env` process: the worker waits up to 3 seconds for confirmation, otherwise it executes the request itself. The number of detached processes is limited by `notification.process_count`.
75
+
76
+
## `RpcExecutor` (adapters)
77
+
78
+
`Ufo\JsonRpcBundle\Server\RpcExecutor::execute(IRpcRequestCarrier $carrier): RpcResponse` is the entry point for extensions that receive requests not through the bundle's route (REST, MCP):
79
+
80
+
* sets the carrier, fires `rpc.request`, then as in a single request;
81
+
* any exception goes through `rpc.error` and is returned as an error response;
82
+
* `rpc.post_response` ā on `kernel.terminate` of the main HTTP request;
83
+
* **does not check the token** ā the adapter does that.
84
+
85
+
## What is stored where during a request
86
+
87
+
| Service | What it contains |
88
+
| --- | --- |
89
+
| `RequestCarrier` | The current carrier (`RpcFromHttp`, `RpcFromCli`, an adapter's carrier) and request objects |
90
+
| `ApiGroupContext` | The API group of the current request |
91
+
| `IRpcSecurity` | The token holder of the current request |
92
+
| `RpcEventFactory` | All events fired for the request (`getEvent()`, `getEvents()`) |
93
+
| `PostResponseQueue` | Requests for which `rpc.post_response` will be fired |
94
+
95
+
In long-running workers these services are reset between requests (`ResetInterface`).