Request lifecycle

← Request lifecycle

Ashterix

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`).