13 — Configuration reference
Every YAML knob, with defaults and recommendations. Place under
config/packages/json_rpc_server.yaml.
Zero-config defaults
json_rpc_server: ~
Equivalent to the full tree below. Everything is optional; override only what you need.
Full tree
json_rpc_server:
# ---------- security ----------
security:
roles_match: any
expose_role_names: true
default_roles: [] # e.g. ['ROLE_USER'] for secure-by-default
public_prefixes: [] # e.g. ['public.', 'health.']
public_methods: [] # e.g. ['ping', 'version']
prefix_roles: {} # e.g. {'admin.': ['ROLE_ADMIN'], 'internal.': ['ROLE_INTERNAL']}
# ---------- request / response shape ----------
max_request_size: 1048576
max_json_depth: 32
http_status:
enabled: false
json:
encode_flags: 96 # JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
headers:
deprecation: 'X-Rpc-Deprecated'
context:
request_id_header: 'X-Request-Id'
# ---------- params / DTOs ----------
params:
allow_positional_dto: false
reject_unknown: true
serializer:
datetime_format: iso8601
date_format: 'Y-m-d'
timezone: ~
# ---------- handlers in DI ----------
handlers:
public: false
shared: false
# ---------- routes ----------
routes:
rpc: { path: /rpc, enabled: true }
stream: { path: /rpc/stream, enabled: true }
mcp_tools: { path: /mcp/tools, enabled: true }
mcp_call: { path: /mcp/call, enabled: true }
# ---------- caching ----------
cache:
default_pool: cache.app
pools: {}
max_readable_key_length: 200
key_prefix: rpc.cache
hash_prefix: rpc
# ---------- rate limiter ----------
rate_limiter:
cache_pool: cache.app
# ---------- streaming ----------
stream:
headers:
X-Accel-Buffering: no
Cache-Control: no-cache
# ---------- profiler ----------
profiler:
enabled: true
# ---------- MCP ----------
mcp:
enabled: false
format_header: 'X-Mcp-Format'
format_query: 'format'
default_format: json
apply_rate_limit: true
expose_all: false
exclude_prefixes: []
exclude_methods: []
whitelist_methods: []
schema_max_depth: 6
markdown:
max_table_rows: 25
max_table_cols: 6
# ---------- parallel batch (opt-in) ----------
parallel_batch:
enabled: false
self_url: ~ # required when enabled, e.g. 'http://127.0.0.1/rpc'
min_batch_size: 2
max_concurrency: 3
budget: 10
budget_store: apcu # apcu | null | <service id>
max_depth: 1
connect_timeout: 0.5
timeout: 10.0
forward_headers: [Authorization, X-Request-Id, X-Forwarded-For, X-Forwarded-Proto, traceparent, tracestate]
# ---------- observability (all opt-in) ----------
logging:
enabled: false
channel: ~
level_started: debug
level_completed: info
level_failed: warning
log_params: true
log_result: false
slow_threshold_ms: ~
sentry:
enabled: false
breadcrumbs: true
tag_method: true
transactions: false
ignore_exceptions:
- Knetesin\JsonRpcServerBundle\Exception\InvalidParamsException
- Knetesin\JsonRpcServerBundle\Exception\InvalidRequestException
- Knetesin\JsonRpcServerBundle\Exception\MethodNotFoundException
- Knetesin\JsonRpcServerBundle\Exception\ParseException
- Knetesin\JsonRpcServerBundle\Exception\AccessDeniedException
- Knetesin\JsonRpcServerBundle\Exception\RateLimitExceededException
opentelemetry:
enabled: false
tracer_name: 'json-rpc'
traces: true
metrics: true
propagate_traceparent: true
record_params: false
record_result: false
record_max_chars: 2048
stream:
record_row_count: true
span_per_row: false
ignore_exceptions: [...] # same default set as Sentry
Per-key reference
security.roles_match
Default any. Used for #[Rpc\Method] when rolesMatch: is omitted.
any— at least one of the roles must be granted.all— every role must be granted.
Per-method override: #[Rpc\Method(rolesMatch: RoleMatch::All)].
security.expose_role_names
Default true (dev-friendly). When true, AccessDeniedException messages
name the missing role(s): “One of the following roles is required: ROLE_X,
ROLE_Y”. Flip to false in prod if your role identifiers leak business
structure (ROLE_BILLING_INTERNAL) — the client then gets a generic
Access denied. false also keeps role identifiers out of discovery:
/mcp/tools entries carry no roles, and the OpenRPC document carries no
x-rpc-roles / x-rpc-roles-match.
security.default_roles / public_prefixes / public_methods / prefix_roles
Default [] / [] / [] / {} — historical “no roles: on attribute means
public” behavior. Set default_roles to flip the bundle into secure-by-default
mode: every method that doesn’t carry its own #[Rpc\Method(roles: [...])]
inherits the listed roles, with public_prefixes and public_methods as the
allowlist for endpoints that should stay anonymous.
prefix_roles lets you narrow defaults per method-name prefix without touching
every handler: e.g. admin.* → ROLE_ADMIN, internal.* → ROLE_INTERNAL,
while everything else still falls back to default_roles. Longest matching
prefix wins, so admin.users. overrides admin. for admin.users.create.
Resolution precedence (first match wins, computed at container compile time):
- attribute carries explicit
roles→ use as-is - method name is in
public_methods→ public - method name starts with any
public_prefixesentry → public - method name matches a
prefix_rolesentry (longest wins) → those roles default_rolesis non-empty → apply default- otherwise → public
json_rpc_server:
security:
default_roles: ['ROLE_USER']
public_prefixes: ['public.', 'health.']
public_methods: ['ping']
prefix_roles:
'admin.': ['ROLE_ADMIN']
'internal.': ['ROLE_INTERNAL']
The resolved roles end up in MethodMetadata::$roles — debug:rpc and the
profiler show the materialized value, not the source.
max_request_size
Default 1048576 (1 MiB). Body size limit in bytes for /rpc, /rpc/stream
and /mcp/call. 0 disables.
Per-method override via #[Rpc\MaxRequestSize(bytes: 65536)]. When the global
limit is non-zero, the bundle’s parser cap is raised at compile time to fit
the largest per-method maximum (so a method declaring a higher limit isn’t
rejected before reaching the dispatcher). When the global limit is 0 (i.e.
uncapped), the parser cap stays 0 regardless of per-method values —
otherwise a single method with a small cap would silently cap every other
method at the parser stage.
Inside a batch, each item is checked against its method’s limit by the item’s
own size (its re-encoded JSON envelope); an oversized item gets a per-item
-32600 and the rest of the batch runs. An item that cannot be re-encoded — a
number beyond float range such as 1e400 decodes to INF — gets a per-item
-32602, since its size cannot be checked. HTTP 413 is returned only when nothing
ran: the body exceeds the parser cap, or a single (non-batch) request exceeds
its method’s limit.
max_json_depth
Default 32. Maximum json_decode nesting depth for incoming RPC / MCP
payloads. Raise only if your clients legitimately send deep structures.
http_status.enabled
Default false. When true, /rpc maps JSON-RPC error.code values to HTTP
status codes (400/403/404/429/500) similar to /rpc/stream. Oversized non-batch
bodies still return 413 even when this flag is off. Batch responses use the highest
status among items. Leave disabled in production JSON-RPC clients unless you
know your callers expect non-200 transports.
json_rpc_server:
http_status:
enabled: true
json.encode_flags
Default 96 (JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES). Bitmask of
json_encode flags for
response bodies. JSON_THROW_ON_ERROR is always OR-ed in by the bundle so
encoding failures never silently produce false.
Common additions:
| Flag | Value | When |
|---|---|---|
JSON_PRETTY_PRINT |
128 |
pretty-print responses (dev) |
JSON_PRESERVE_ZERO_FRACTION |
1024 |
force 1.0 over 1 in floats |
JSON_HEX_TAG |
1 |
escape < > for safe HTML embedding |
headers.deprecation
Default X-Rpc-Deprecated. Custom HTTP header that carries
human-readable per-method deprecation reasons in the form
method.name: reason; other.method: …. The standard Deprecation: true
(RFC 9745) is always sent alongside. Override when your platform policy
bans X-* prefixes.
context.request_id_header
Default X-Request-Id. HTTP header read to populate Context::$requestId.
Resolution order (first non-empty wins):
- Cached value from a prior call in the same request — every item in a JSON-RPC batch shares one requestId.
- The configured header — lets an API gateway pin its correlation id end-to-end.
- Freshly generated
bin2hex(random_bytes(8)).
Set to an empty string to disable header lookup entirely.
params.allow_positional_dto
Default false. Whether handlers with a single DTO parameter accept
positional JSON-RPC params ("params": [...]). Forbidden by default because
positional params lock the DTO constructor order into the public API.
Per-method override: #[Rpc\Method(allowPositionalDto: true)].
params.reject_unknown
Default true. Whether DTO denormalization rejects unknown fields. The
default catches client typos and stale legacy keys. Override per-method via
#[Rpc\Method(rejectUnknown: false)] for backward-compatible endpoints.
serializer.datetime_format
Default iso8601. Output format for DateTimeInterface.
| Value | Output | Input accepted |
|---|---|---|
iso8601 |
2026-05-21T15:00:00+03:00 |
ISO, RFC, "yesterday", etc. |
timestamp |
unix seconds (int) | int → seconds, string → DateTimeImmutable |
timestamp_ms |
unix milliseconds (int) | int → milliseconds, string → DateTimeImmutable |
any date() format |
per the format | matching string strict-first, then lenient |
serializer.date_format
Default Y-m-d. Output format for Type\Date (date without time). On
input strings are tried strictly first, then through DateTimeImmutable as
fallback (so 2026-05-21, 21.05.2026, 2026/05/21 all parse). Numbers
are accepted as timestamps and truncated to date in the configured timezone.
serializer.timezone
Default null (source-value timezone untouched). When set (e.g. UTC) the
timezone is applied when normalizing DateTimeInterface to a string and
when truncating timestamps to dates. Setting to UTC is strongly
recommended in cross-TZ deployments.
handlers.public
Default false. Whether RPC handler services are public in the container.
Handlers are reached only via the bundle’s ServiceLocator — false keeps
them off the public service API. Flip to true only if you need to fetch a
handler directly from Container::get().
handlers.shared
Default false. Whether handler services are shared (singleton). Default
false is required for long-running PHP runtimes (RoadRunner,
FrankenPHP, Swoole) — a shared handler with mutable state would leak data
between requests. Flip to true only if your handlers are guaranteed
stateless and you want the per-process instantiation cost saved.
routes.{name}.path / routes.{name}.enabled
These keys only affect routes that Symfony already loads from
config/routes/json_rpc_server.yaml (created by the Flex recipe or added
manually — see Getting started).
Each route node accepts either a string (= path, enabled) or an object
{ path: ..., enabled: false }. enabled: false skips the bundled route
entirely — useful if you define your own that points at the bundle
controller.
cache.default_pool
Default cache.app. PSR-6 pool service id used when a method’s
#[Rpc\Cache] doesn’t specify pool:.
cache.pools
Default {}. Named map of additional pools that #[Rpc\Cache(pool: 'name')]
can reference. Values are service ids.
json_rpc_server:
cache:
pools:
hot: cache.redis_hot
warm: cache.redis_warm
cache.max_readable_key_length
Default 200. Maximum length of a human-readable cache key. Longer keys
(or those carrying characters PSR-6 reserves) are hashed into
<hash_prefix>.<sha1>. Lower for backends with strict key budgets, raise
when keys would benefit from being greppable.
cache.key_prefix
Default rpc.cache. Prefix prepended to every cache key the bundle stores.
Change when multiple instances of the bundle share a pool and you need
separate namespaces (e.g. json_rpc_server.cache.tenant_a / json_rpc_server.cache.tenant_b).
cache.hash_prefix
Default rpc. Prefix used when a cache key gets hashed. Final key shape:
<hash_prefix>.<sha1>.
rate_limiter.cache_pool
Default cache.app. PSR-6 pool used as the Symfony rate-limiter storage.
Point at a dedicated Redis-backed pool for production workloads that need
counters shared across processes.
stream.headers
Map of additional response headers to set on every streamed response. Set
a value to ~ (null) to drop a default. Default disables nginx output
buffering and HTTP caches so each chunk reaches the client immediately:
json_rpc_server:
stream:
headers:
X-Accel-Buffering: no
Cache-Control: no-cache
# Add your own:
Access-Control-Allow-Origin: '*'
# Drop a default:
X-Accel-Buffering: ~
profiler.enabled
Default true. Enables the Symfony Web Profiler integration. No-op outside
kernel.debug, so safe to leave on in prod.
mcp.enabled
Default true. Set false to remove MCP services and routes entirely.
mcp.format_header / mcp.format_query
Defaults X-Mcp-Format and format. HTTP header / query-string name read
to pick the MCP result format per request (highest priority in the format
resolution chain). Override when your platform/proxy strips or rewrites
X-* headers.
mcp.default_format
Default json. Result format used when neither the header / query / per-
method attribute sets one. One of: json, pretty_json, markdown,
plain, toon.
mcp.apply_rate_limit
Default true. Whether to apply #[Rpc\RateLimit] when a method is
called via /mcp/call, so the MCP endpoint is limited like /rpc. Set
false only when /mcp/call is reachable exclusively by a trusted internal
agent.
mcp.expose_all / exclude_prefixes / exclude_methods / whitelist_methods
The MCP filter chain — see chapter 08 for the priority order.
mcp.schema_max_depth
Default 6. Maximum nesting depth JsonSchemaBuilder walks into a DTO.
Guards against self-referencing DTOs that would otherwise recurse forever.
mcp.markdown.max_table_rows / max_table_cols
Defaults 25 and 6. Above these the markdown MCP format falls back to
JSON instead of rendering an unwieldy table.
parallel_batch.enabled
Default false — parallel batch is off unless you turn it on. See
Methods → parallel batches
before enabling. Requires symfony/http-client, self_url and a non-empty
kernel.secret (framework.secret), which keys the HMAC that authenticates
the client IP forwarded to sub-calls.
parallel_batch.self_url
Default null; required when enabled: true. The absolute http:// /
https:// URL sub-calls are POSTed to, including the RPC path — e.g.
http://127.0.0.1/rpc, or a dedicated worker pool such as
http://127.0.0.1/internal/rpc-fanout. The container build fails when it is
missing or not an absolute http(s) URL. An env-var value can only be checked
at runtime: if it is not an absolute http(s) URL, an error is logged and
batches run sequentially — single calls are unaffected. It is never derived
from the incoming request: the Host header is client-controlled and
sub-calls carry the forwarded Authorization header.
json_rpc_server:
parallel_batch:
enabled: true
self_url: '%env(RPC_SELF_URL)%'
parallel_batch.budget / budget_store
Defaults 10 and apcu. budget caps in-flight sub-calls across all
concurrent batches; 0 disables the cap. budget_store: apcu checks APCu at
runtime in the serving process: when APCu is unusable there, batches run
sequentially and a warning is logged once per process. null means no
system-wide cap; any other value is the id of a service implementing
BudgetTrackerInterface.
parallel_batch.forward_headers
Incoming headers re-sent on every sub-call. Default: Authorization,
X-Request-Id, X-Forwarded-For, X-Forwarded-Proto, traceparent,
tracestate. Cookie is not in the default: with PHP’s native (locking)
session handler the parent holds the session lock and every sub-call blocks
on it until timeout. Cookie-session apps add Cookie explicitly, and only
with non-locking or read-only sessions. The internal headers
X-Rpc-Fanout-Depth, X-Rpc-Fanout-Client-Ip and X-Rpc-Fanout-Signature
are always set by the bundle and never copied from the client.
Observability
See chapter 11 for the logging.*, sentry.*,
and opentelemetry.* keys with full context.