# Directives > Source: https://docs.erpc.cloud/operation/directives > Send an HTTP header or query param and change routing, caching, validation, or consensus for exactly that one request — no restarts, no config changes. > Format: machine-readable markdown export of the docs page above. > All collapsible AI sections are inlined and fully expanded. # Directives Every request can carry its own instructions. Add an `X-ERPC-*` header or a `?param=` query string and eRPC instantly shifts routing, caching, integrity checks, or consensus behavior for that call only. No server restart. No config change. These directives can also be pinned as `directiveDefaults` on any network so every call starts with a sensible baseline. For production-facing projects, [`allowClientDirectives`](/config/projects.llms.txt) can restrict which directives clients may send — block cache-bypass or upstream-pinning headers while keeping integrity directives available. When eRPC is itself the edge (no trusted proxy in front to strip headers), the restriction can be scoped **per caller** instead of per project: set `allowClientDirectives: ""` on the project to deny everyone by default, then re-grant on the individual auth strategies that trusted callers authenticate with: ```yaml projects: - id: main allowClientDirectives: "" # default: nobody auth: strategies: - type: secret # operators: every directive allowClientDirectives: "*" secret: { id: ops, value: ${OPS_TOKEN} } - type: secret # partners: upstream pinning only allowClientDirectives: "use-upstream" secret: { id: partner, value: ${PARTNER_TOKEN} } - type: jwt # everyone else: inherits "" → denied jwt: { ... } ``` The grant travels on the authenticated `User`, so it is only ever issued by the strategy that actually verified the caller's credential. A caller whose identity comes from [`trustUserIdHeader`](/config/projects.llms.txt) carries no grant and always falls back to the project value — spoofing the identity header cannot widen directive access. After each response, eRPC echoes back what actually happened — which upstream won, cache hit or miss, retry and hedge counts — so you always know exactly what the proxy did. ## Quick taste Per request, the way directives are actually used — pin one call to specific upstreams via header or query string: **File:** `terminal` ```bash # via header: only Alchemy upstreams may serve this request curl https://rpc.example.com/main/evm/1 \\ -H 'X-ERPC-Use-Upstream: alchemy-*' \\ -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' # same directive as a query parameter curl 'https://rpc.example.com/main/evm/1?use-upstream=alchemy-*' \\ -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' ``` Directives that make sense as a permanent baseline can be pinned per network with `directiveDefaults` — headers and query params still override them per request: **Config path:** `projects[].networks[].directiveDefaults` **YAML — `erpc.yaml`:** ```yaml projects: - id: main networks: - architecture: evm evm: { chainId: 1 } directiveDefaults: # retry when upstream returns null/empty (e.g. not-yet-indexed block) retryEmpty: true # reject responses whose block number is behind the known tip enforceHighestBlock: true ``` **TypeScript — `erpc.ts`:** ```typescript projects: [{ id: "main", networks: [{ architecture: "evm", evm: { chainId: 1 }, directiveDefaults: { // retry when upstream returns null/empty (e.g. not-yet-indexed block) retryEmpty: true, // reject responses whose block number is behind the known tip enforceHighestBlock: true, }, }], }] ``` ## Agent reference Copy one of these prompts into your AI agent session (Claude Code, Cursor, …) — each one points the agent at this page's machine-readable reference so it can do the work correctly: **Prompt Example #1: set sane directive defaults for a new network** ```text I'm adding a new EVM network to my eRPC config and want to pin sensible directiveDefaults — retryEmpty, enforceHighestBlock, enforceNonNullTaggedBlocks, and useUpstream routing for archival calls. Read the full reference first: https://docs.erpc.cloud/operation/directives.llms.txt ``` **Prompt Example #2: audit and tighten my existing directive defaults** ```text Review the directiveDefaults blocks in my eRPC config: check which integrity directives are on by default but may be incompatible with non-standard chains (zkSync, Filecoin, Kaia style), flag any retryPending: true set globally (dangerous), and suggest per-network overrides. Read the reference: https://docs.erpc.cloud/operation/directives.llms.txt ``` **Prompt Example #3: pin an archival upstream for indexer traffic** ```text My indexer sends eth_getLogs and eth_getBlockReceipts requests that should always hit an archival upstream tagged family:archival in my eRPC config. Configure directiveDefaults.useUpstream so those calls are pinned to that group and set the network's integrity level for indexing correctness. Reference: https://docs.erpc.cloud/operation/directives.llms.txt ``` **Prompt Example #4: debug why X-ERPC-Retry-Empty is not retrying** ```text My requests have X-ERPC-Retry-Empty: true in the header but eRPC is not retrying empty results. Walk me through why this might happen (boolean parse rules, EmptyResultMaxAttempts cap, gRPC path limitation) and how to verify via the X-ERPC-Attempts response header that retries are actually firing. Work with my existing eRPC config. Reference: https://docs.erpc.cloud/operation/directives.llms.txt ``` **Prompt Example #5: reduce diagnostic header noise in production logs** ```text Our load balancer logs every X-ERPC-Upstreams response header and it's adding megabytes to our access logs. Configure executionHeaders on the server to suppress the per-attempt trace while keeping the counter headers for dashboards. Work with my existing eRPC config. Reference: https://docs.erpc.cloud/operation/directives.llms.txt ``` **Prompt Example #6: lock down client directives on a public-facing project** ```text My eRPC project serves public traffic and I want to prevent clients from sending X-ERPC-Skip-Cache-Read or X-ERPC-Use-Upstream headers to bypass caching or pin upstreams. Configure allowClientDirectives so integrity directives are still allowed but operational ones are blocked. Reference: https://docs.erpc.cloud/operation/directives.llms.txt ``` --- ### Directives — full agent reference ### How it works **Parsing pipeline.** For every HTTP request eRPC runs three steps. First, `ApplyDirectiveDefaults` copies any `directiveDefaults` config block into the request struct (lowest priority). Second, `SetAllowClientDirectiveMatcher` stores a pre-compiled matcher function from the project-level `allowClientDirectives` pattern (compiled once at project registration via `NewWildcardMatcher`). Third, `EnrichFromHttp` scans all registered header and query names, skipping any directive whose query-param key is rejected by the matcher. If no directives are present it returns immediately after extracting User-Agent — zero allocations, zero locks. When directive inputs are present the struct is cloned before mutation so batch sub-requests that share the same pointer do not race. Precedence from lowest to highest: `directiveDefaults` config → HTTP header → URL query parameter. A query-param value always wins over the same header, which always wins over config. `ApplyDirectiveDefaults` is idempotent — once `r.directives` is non-nil every subsequent call is a no-op, so per-request overrides can never be clobbered by a second config pass. Source: [`common/request.go:563-676`](https://github.com/erpc/erpc/blob/main/common/request.go#L563-L676). **Boolean parsing rule.** For every boolean directive header the only truthy value is the exact string `"true"` (case-insensitive, whitespace stripped). `"1"` and `"yes"` are not truthy. `X-ERPC-Force-Trace` is handled by the tracing subsystem separately and accepts all three. Always use `"true"` to avoid this asymmetry. Confirmed: [`common/request_test.go:406-427`](https://github.com/erpc/erpc/blob/main/common/request_test.go#L406-L427). **`directiveDefaults` placement.** Settable at `projects[].networks[].directiveDefaults` (per network) or `projects[].networks[].failsafe.directiveDefaults` (failsafe scope). Every `*bool` field has three states: nil (omitted — request field stays at Go zero `false`), pointer to `false` (explicitly written), pointer to `true`. The nil check in `ApplyDirectiveDefaults` is a "was this field set?" guard, not a multi-level inheritance chain. **gRPC path.** `EnrichFromHttp` is never called on the gRPC path. gRPC callers receive only `directiveDefaults`; per-request header overrides are not available. Source: [`erpc/request_processor.go:35-67`](https://github.com/erpc/erpc/blob/main/erpc/request_processor.go#L35-L67). ### Config schema Config struct: [`common/config.go:2105-2152`](https://github.com/erpc/erpc/blob/main/common/config.go#L2105-L2152). Applied by `ApplyDirectiveDefaults` at [`common/request.go:563-676`](https://github.com/erpc/erpc/blob/main/common/request.go#L563-L676). #### Complete directive registry | # | HTTP header | Query param | Type | Config field | Default | Effect | Consumed at | |---|---|---|---|---|---|---|---| | 1 | `X-ERPC-Retry-Empty` | `retry-empty` | bool | `retryEmpty` | `false` | Retry on null/empty upstream response. Subject to `EmptyResultMaxAttempts` cap. | [`erpc/network_executor.go:427-442`](https://github.com/erpc/erpc/blob/main/erpc/network_executor.go#L427-L442) | | 2 | `X-ERPC-Retry-Pending` | `retry-pending` | bool | `retryPending` | `false` (struct comment "true by default" is **stale**) | Retry `eth_getTransactionReceipt` / `eth_getTransactionByHash` / `eth_getTransactionByBlockHashAndIndex` / `eth_getTransactionByBlockNumberAndIndex` while tx is pending. `EmptyResultMaxAttempts` cap applies. | [`erpc/network_executor.go:445-459`](https://github.com/erpc/erpc/blob/main/erpc/network_executor.go#L445-L459) | | 3 | `X-ERPC-Skip-Cache-Read` | `skip-cache-read` | string (bool in config YAML normalized to string) | `skipCacheRead` | `""` (no skip) | `"true"` = skip all; `"false"`/`""` = use cache; other string = connector-ID WildcardMatch pattern. YAML `true`/`false` normalized to strings via `fmt.Sprintf("%v", v)`. Header NOT trimmed; query IS trimmed. | [`common/request.go:898-914`](https://github.com/erpc/erpc/blob/main/common/request.go#L898-L914) | | 4 | `X-ERPC-Use-Upstream` | `use-upstream` | string | `useUpstream` | `""` (no filter) | Selector matched against upstream `id`; for purely-positive patterns also against upstream `Tags` (any tag match wins). Negated patterns (`!`) match ID only. Header NOT trimmed; query IS trimmed. No match → `ErrUpstreamsExhausted`. Activates selector-scoped served-tip partitioning. Stateful methods with multiple matching upstreams fail with `ErrNotImplemented`. | [`upstream/upstream.go:1526-1534`](https://github.com/erpc/erpc/blob/main/upstream/upstream.go#L1526-L1534) | | 5 | `X-ERPC-Skip-Interpolation` | `skip-interpolation` | bool | `skipInterpolation` | `false` | Suppresses block-tag → hex substitution in forwarded params. Internal block refs still computed/cached. | [`architecture/evm/json_rpc.go:132`](https://github.com/erpc/erpc/blob/main/architecture/evm/json_rpc.go#L132) | | 6 | `X-ERPC-Skip-Consensus` | `skip-consensus` | bool | `skipConsensus` | `false` | Bypasses consensus branch; uses standard `retry(hedge(upstreamSweep))`. Retry/hedge/breaker/timeout still apply. | [`erpc/network_executor.go:179-188`](https://github.com/erpc/erpc/blob/main/erpc/network_executor.go#L179-L188) | | 7 | `X-ERPC-Enforce-Highest-Block` | `enforce-highest-block` | bool | `enforceHighestBlock` | **`true`** (SetDefaults) | `eth_getBlockByNumber` with `"latest"`/`"finalized"`: re-routes if response is behind known highest block. Skipped for cached responses. | [`architecture/evm/eth_getBlockByNumber.go:111-170`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L111-L170) | | 8 | `X-ERPC-Enforce-GetLogs-Range` | `enforce-getlogs-range` | bool | `enforceGetLogsBlockRange` | **`true`** (SetDefaults) | Pre-forward: rejects `eth_getLogs`/`trace_filter` if block range exceeds `getLogsMaxAllowedRange`. Consumed via legacy `EvmIntegrityConfig` (not a true per-request override — see Edge cases). | [`architecture/evm/eth_getLogs.go:280-288`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getLogs.go#L280-L288) | | 9 | `X-ERPC-Enforce-Non-Null-Tagged-Blocks` | `enforce-non-null-tagged-blocks` | bool | `enforceNonNullTaggedBlocks` | **`true`** (SetDefaults) | Treats null block for tag-based `eth_getBlockByNumber` as an error. Disable for chains that legitimately return null for certain tags (e.g., zkSync). | [`architecture/evm/eth_getBlockByNumber.go:304`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L304) | | 10 | `X-ERPC-Integrity` | `integrity` | string | — | `""` | Per-request data-integrity selection — a level (`off`/`intrinsic`/`corroborated`/`authoritative`) or a configured profile name; honored only when the network's integrity `headerMode` permits. See [Integrity checks](/config/failsafe/integrity.llms.txt). | [`architecture/evm/hooks.go`](https://github.com/erpc/erpc/blob/main/architecture/evm/hooks.go) | #### `server.executionHeaders` — response diagnostic header mode | Value | Effect | Source | |---|---|---| | `"all"` (default) | Full counter headers + `X-ERPC-Upstreams` per-attempt trace | [`erpc/http_server.go:1081-1086`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1081-L1086) | | `"summary"` | All counter headers; `X-ERPC-Upstreams` trace suppressed only | [`erpc/http_server.go:1105-1127`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1105-L1127) | | `"off"` | No `X-ERPC-*` diagnostics (version/commit still emitted) | [`erpc/http_server.go:1105-1127`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1105-L1127) | ### Worked examples All patterns below are distilled from real production fleets; comments explain the non-obvious choices. **1. Global network defaults: retryEmpty on, retryPending explicitly off.** Production fleets set `retryEmpty: true` at `networkDefaults` so every network retries null/empty upstream responses (e.g., a not-yet-indexed block) without per-network config. `retryPending: false` is explicit because the struct's stale "true by default" comment causes confusion — production configs state it clearly: **Config path:** `projects[].networkDefaults.directiveDefaults` **YAML — `erpc.yaml`:** ```yaml networkDefaults: directiveDefaults: # Retry null/empty results globally — covers block-availability races # on eth_getLogs, eth_getBlockByNumber, eth_getTransactionReceipt. retryEmpty: true # retryPending default is false despite stale struct comment; # pin it explicitly so intent is clear to future maintainers. retryPending: false ``` **TypeScript — `erpc.ts`:** ```typescript networkDefaults: { directiveDefaults: { // Retry null/empty results globally — covers block-availability races // on eth_getLogs, eth_getBlockByNumber, eth_getTransactionReceipt. retryEmpty: true, // retryPending default is false despite stale struct comment; // pin it explicitly so intent is clear to future maintainers. retryPending: false, }, } ``` **2. Non-standard chain: relax tagged-block enforcement.** zkSync Era and similar ZK-rollups can legitimately return null for certain block tags, so they disable `enforceNonNullTaggedBlocks` per-network while inheriting all other defaults: **Config path:** `projects[].networks[].directiveDefaults` **YAML — `erpc.yaml`:** ```yaml networks: - evm: chainId: 324 # zkSync Era mainnet directiveDefaults: # zkSync can return null for certain block tags — don't treat as error enforceNonNullTaggedBlocks: false ``` **TypeScript — `erpc.ts`:** ```typescript networks: [{ evm: { chainId: 324, // zkSync Era mainnet }, directiveDefaults: { // zkSync can return null for certain block tags — don't treat as error enforceNonNullTaggedBlocks: false, }, }] ``` **3. Indexer workload — full receipt integrity.** When backfilling a chain for an indexer you want to catch any upstream that sends partial or misordered receipts. Configure the network's [`integrity`](/config/failsafe/integrity.llms.txt) block so every call on that network is validated automatically, and keep the kept enforcement directives on at `directiveDefaults`: **Config path:** `projects[].networks[].directiveDefaults` **YAML — `erpc.yaml`:** ```yaml directiveDefaults: retryEmpty: true # tx-receipt polling — only enable per-indexer-network, not globally retryPending: true enforceHighestBlock: true enforceNonNullTaggedBlocks: true ``` **TypeScript — `erpc.ts`:** ```typescript directiveDefaults: { retryEmpty: true, // tx-receipt polling — only enable per-indexer-network, not globally retryPending: true, enforceHighestBlock: true, enforceNonNullTaggedBlocks: true, } ``` **4. Per-request upstream pinning.** A dApp wants to read its own just-submitted transaction from the same upstream it used to broadcast, bypassing the normal load-balanced pool. Send the header at call time — no config change needed: ```http POST /1/main HTTP/1.1 X-ERPC-Use-Upstream: alchemy-mainnet X-ERPC-Retry-Pending: true ``` The upstream selector supports wildcards and tags: `"alchemy-*"`, `"alchemy-mainnet|quicknode-*"`, `"family:archival"` (tag match), `"!drpc"` (exclude one by ID). **5. Per-call cache bypass for fresh data.** A price-feed service needs uncached `eth_call` results on every tick. Send `X-ERPC-Skip-Cache-Read: true` per request; write-back still fires so the next caller gets the fresh value from cache: ```http POST /1/main HTTP/1.1 X-ERPC-Skip-Cache-Read: true ``` To bypass only the in-memory tier and keep Redis warm reads: `X-ERPC-Skip-Cache-Read: memory*`. **6. Skipping consensus for internal tooling.** A monitoring script wants low-latency reads and trusts a single upstream. Bypass the consensus quorum for that call: ```http POST /1/main HTTP/1.1 X-ERPC-Skip-Consensus: true ``` Retry, hedge, circuit-breaker, and timeout still apply — only the dispute/agreement step is skipped. ### Request/response behavior **Request headers — canonical truthy value.** Every boolean directive header requires `"true"` (any case, optional surrounding whitespace). `"1"` and `"yes"` evaluate to `false` and silently have no effect. Exception: `X-ERPC-Force-Trace` (tracing subsystem, not a directive) accepts `"true"`, `"1"`, or `"yes"`. Always use `"true"`. **Directive-adjacent request inputs** (not in the directive registry): | Input | Kind | Values / behavior | Source | |---|---|---|---| | `X-ERPC-Force-Trace` | header | `"true"`, `"1"`, or `"yes"` → bypass OTel trace sampling for that request (attribute `erpc.force_trace = true`) | [`common/tracing_util.go:107-115`](https://github.com/erpc/erpc/blob/main/common/tracing_util.go#L107-L115) | | `force-trace` | query param | same three truthy values | [`common/tracing_core.go:31`](https://github.com/erpc/erpc/blob/main/common/tracing_core.go#L31) | | `user-agent` | query param | overrides `User-Agent` header for agent-name metrics tracking | [`common/request.go:1299-1314`](https://github.com/erpc/erpc/blob/main/common/request.go#L1299-L1314) | | `User-Agent` | header | stored raw or simplified per `project.userAgentMode`; drives agent-name metric labels | [`common/request.go:1300-1311`](https://github.com/erpc/erpc/blob/main/common/request.go#L1300-L1311) | | `networkId` | JSON body field | `"evm:42161"`-style network selection when architecture/chain are absent from the URL path | [`erpc/http_server.go:613-634`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L613-L634) | **Response headers always emitted (every HTTP response, not controlled by `executionHeaders`):** | Header | Value | Source | |---|---|---| | `Content-Type` | `application/json` | [`erpc/http_server.go:259`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L259) | | custom `server.responseHeaders` | static values, env-expanded at startup | [`erpc/http_server.go:263-266`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L263-L266) | **Execution diagnostic headers** (controlled by `server.executionHeaders`; single responses get the per-response set, batch responses get ONE aggregated set — counters summed across sub-calls, `X-ERPC-Duration` = slowest sub-call, no `X-ERPC-Upstream`): | Header | Value | When | |---|---|---| | `X-ERPC-Version` | eRPC version string | always | | `X-ERPC-Commit` | git commit SHA | always | | `X-ERPC-Attempts` | total physical ops (upstream + cache) | always | | `X-ERPC-Upstream-Attempts` / `X-ERPC-Upstream-Retries` / `X-ERPC-Upstream-Hedges` | counters | always | | `X-ERPC-Network-Attempts` / `X-ERPC-Network-Retries` / `X-ERPC-Network-Hedges` | counters | always | | `X-ERPC-Cache-Attempts` / `X-ERPC-Cache-Retries` / `X-ERPC-Cache-Hedges` | counters | only when > 0 | | `X-ERPC-Consensus-Slots` / `X-ERPC-Consensus-Disputes` / `X-ERPC-Consensus-Low-Participants` | counters | only when > 0 | | `X-ERPC-Cache` | `HIT` or `MISS`; on batch responses also `PARTIAL:` (n sub-calls served from cache) | when response metadata present | | `X-ERPC-Upstream` | winning upstream id | single responses only, when known | | `X-ERPC-Duration` | milliseconds (batch: the slowest sub-call) | NormalizedResponse only | | `X-ERPC-Upstreams` | per-attempt trace: `upstreamId=reason:outcome:durationMs[:won]` segments joined by `;`. Known `reason` values: `primary`, `hedge`, `retry`, `consensus_slot`, `sweep`. Known `outcome` values: `success`, `timeout`, `exec_revert`, `rate_limited`. `:won` suffix appended only when the attempt's response contributed to the final answer (in a batch: to its own sub-call's answer). Batch: segments concatenate in sub-call order, capped at 50 with `X-ERPC-Upstreams-Truncated: `. | `all` mode only; when `len(attempts) > 0` | Example `X-ERPC-Upstreams` value: `alchemy=primary:success:50ms:won;quicknode=hedge:timeout:5000ms;drpc=consensus_slot:exec_revert:20ms`. `reason` precedence when several apply to one attempt: `consensus_slot` > `hedge` > `sweep` > `retry` > `primary` — the outermost fan-out cause wins, so an attempt that hedged or retried *inside* a consensus participant slot still reads `consensus_slot` (the attempt record's `IsHedge`/`IsRetry` fields preserve the inner mechanics). **Cost / billing headers** (opt-in via `server.costHeaders: true`; emitted on single AND batch responses; omitted on early errors that never routed a call): | Header | Value | |---|---| | `X-ERPC-Calls` | routed JSON-RPC sub-calls in this HTTP response (batch size; `1` for a single request) | | `X-ERPC-Billable` | how many of them were billable. Successful, empty-but-valid and execution-revert responses bill (cache hits included — their *cost* is zero, not their billability; a revert is real work the node performed); protocol, transport, rate-limit and cancellation failures do not | | `X-ERPC-Methods` | distinct JSON-RPC methods, sorted, comma-joined | | `X-ERPC-Credits` | `vendor:method=` segments, sorted, `;`-joined — the vendor credit units consumed by **every physical upstream attempt** (retries, hedges and consensus slots included; skipped / breaker-open attempts provably never dialed and cost 0). Read from the per-request aggregate (`NormalizedRequest.CreditUnitsByVendor`). Omitted when nothing accrued (e.g. pure cache hits). Units come from vendors implementing `CreditUnitsProvider` (built-in tables: Alchemy CUs, QuickNode credits, dRPC CUs), merged per method with `providers[].settings.creditUnits` overrides; vendors with no table default to a **flat 1 credit per request** (opt out with `creditUnits: {"*": 0}`) | | `X-ERPC-Credits-Total` | grand total credit units across all vendors and sub-calls in this response; present alongside `X-ERPC-Credits` | | `X-ERPC-Credits-Version` | the eRPC version the built-in vendor tables shipped with; only present alongside `X-ERPC-Credits` | Example: `X-ERPC-Calls: 5` · `X-ERPC-Billable: 4` · `X-ERPC-Cache: PARTIAL:2` · `X-ERPC-Credits: alchemy:eth_call=52;quicknode:eth_getLogs=40` · `X-ERPC-Credits-Total: 92`. Cost-header gotchas: emitted only for routed requests — early parse/auth errors get none (there is no routed call to account for). Pricing is the **vendor's** logic (`common.CreditUnitsProvider.CreditUnits(req, upstreamCfg)`; nothing is hard-coded in the eRPC layer). A timed-out attempt still accrues credits — it dialed the vendor — while `skipped`/`breaker_open` attempts provably never left the process and cost 0. Vendors without pricing cost a flat 1 credit per request unless opted out (`creditUnits: {"*": 0}`). Source: `writeCostHeaders` in [`erpc/http_server.go`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go), `attemptCreditUnits` in [`upstream/upstream.go`](https://github.com/erpc/erpc/blob/main/upstream/upstream.go), tests in [`erpc/http_server_cost_test.go`](https://github.com/erpc/erpc/blob/main/erpc/http_server_cost_test.go) and [`upstream/credit_units_test.go`](https://github.com/erpc/erpc/blob/main/upstream/credit_units_test.go). **Error shapes produced by directive checks.** Failed validation directives return `ErrEndpointContentValidation`. A `UseUpstream` selector that matches no upstream returns `ErrUpstreamsExhausted` (wrapping `ErrUpstreamNotAllowed` per candidate) — there is no distinct "selector matched nothing" error code. ### Best practices - **Start with `enforceHighestBlock: true`** — it is on by default and catches the most common upstream data-quality issues with negligible overhead. - **Raise the network's [`integrity`](/config/failsafe/integrity.llms.txt) level only for indexing workloads** — higher levels (e.g. bloom recomputation, log-field checks) are CPU-intensive and penalize latency for general-purpose proxy traffic. Use `X-ERPC-Integrity` to opt a single request into a higher level. - **Use `retryEmpty: true` for block-polling calls** but verify `EmptyResultMaxAttempts` is set appropriately; unbounded retries on a degraded upstream can exhaust the timeout budget. - **Never set `retryPending: true` globally** — it converts every pending-tx lookup into a polling loop. Pin it per request (`X-ERPC-Retry-Pending: true`) or to a dedicated network for transaction-tracking flows. - **Pin `useUpstream` at config via `directiveDefaults` for known-good archival nodes** rather than relying on callers to send the header — this prevents a misconfigured client from silently routing archival calls to full nodes. - **Always send `"true"`, never `"1"` or `"yes"`**, for `X-ERPC-*` boolean directive headers. - **On gRPC, per-request overrides are not available.** Wire all desired defaults into `directiveDefaults` in config; `EnrichFromHttp` is never called on the gRPC path. - **Lock down client directives on public-facing projects** with `allowClientDirectives: "!skip-cache-read & !use-upstream"` to prevent clients from bypassing your cache or pinning to specific upstreams while still allowing integrity directives. ### Edge cases & gotchas 1. **`RetryPending` default is `false`, not `true`.** The struct comment at `common/request.go:125` ("true by default") is stale. No `SetDefaults` entry exists. Must be explicitly enabled via `directiveDefaults.retryPending: true` or `X-ERPC-Retry-Pending: true`. 2. **`X-ERPC-Use-Upstream` header is NOT trimmed; query IS.** A header with leading/trailing spaces will not match any upstream. Source: [`common/request.go:741`](https://github.com/erpc/erpc/blob/main/common/request.go#L741) vs [`:812`](https://github.com/erpc/erpc/blob/main/common/request.go#L812). 3. **`"1"` and `"yes"` are NOT truthy for directive headers.** Only `"true"` (any case). This differs from `X-ERPC-Force-Trace`. Source: [`common/request_test.go:406-427`](https://github.com/erpc/erpc/blob/main/common/request_test.go#L406-L427). 4. **`UseUpstream` failure produces `ErrUpstreamsExhausted`, not a selector-specific error.** Operators must parse the error message text to diagnose selector mismatches. Source: [`common/errors.go:1432-1440`](https://github.com/erpc/erpc/blob/main/common/errors.go#L1432-L1440). 5. **`X-ERPC-Skip-Consensus: false` actively disables consensus bypass.** Sending the header with the value `"false"` is NOT the same as omitting the header. A header value `"false"` parses to `false` and overrides a `directiveDefaults.skipConsensus: true` config entry — the consensus branch then runs normally. Omitting the header leaves `SkipConsensus` at its config-default value. Tested: [`common/request_test.go:409`](https://github.com/erpc/erpc/blob/main/common/request_test.go#L409), [`erpc/skip_consensus_directive_test.go:125-153`](https://github.com/erpc/erpc/blob/main/erpc/skip_consensus_directive_test.go#L125-L153). 6. **`EnforceGetLogsBlockRange` cannot be overridden per-request via HTTP.** `SetDefaults` copies `DirectiveDefaults` values into `Evm.Integrity`; the architecture layer reads from the network config struct, not the per-request directive. Source: [`common/defaults.go:1957-1964`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1957-L1964). 7. **Batch responses emit ONE aggregated `X-ERPC-*` header set.** Counters sum across sub-calls, `X-ERPC-Cache` becomes `PARTIAL:` when mixed, `X-ERPC-Duration` is the slowest sub-call, the trace caps at 50 segments (`X-ERPC-Upstreams-Truncated` carries the overflow count), and the single-winner `X-ERPC-Upstream` is not emitted. Per-sub-call attribution still needs the per-response `id` fields in the body. 8. **`executionHeaders: summary` removes only `X-ERPC-Upstreams`.** All counter and metadata headers still emit. Use `"off"` to suppress everything. 9. **Selector-scoped served-tip partitions are capped.** Beyond `maxServedTipPartitions` per network, no partition is created and the stateless fallback is used silently — no error. Source: [`erpc/networks.go:429`](https://github.com/erpc/erpc/blob/main/erpc/networks.go#L429). 10. **`nil *bool` in `DirectiveDefaultsConfig` ≠ `false *bool`.** Nil means "not set — skip"; a `*false` pointer means "explicitly disable". Only non-nil pointers are applied in `ApplyDirectiveDefaults`. Source: [`common/request.go:570-580`](https://github.com/erpc/erpc/blob/main/common/request.go#L570-L580). 11. **`allowClientDirectives` filters HTTP-supplied directives only.** Config-set `directiveDefaults` always apply regardless of the filter. The pattern is pre-compiled at project registration via `NewWildcardMatcher` and evaluated against each directive's query-param key (e.g. `skip-cache-read`, `use-upstream`). `nil` = all allowed; `""` = none allowed; `"!skip-cache-read & !use-upstream"` = all except those two. Does not filter `X-ERPC-Force-Trace` (processed before project resolution). Source: `isDirectiveAllowed` method on `NormalizedRequest` in `common/request.go`, `NewWildcardMatcher` in `common/matcher.go`, `AllowClientDirectives` in `common/config.go`. See [projects config](/config/projects.llms.txt). ### Observability Directives have no dedicated Prometheus metrics; their effects appear in existing counters. | Metric | Type | When it fires | |---|---|---| | `erpc_upstream_request_retries_total{reason="empty_result"}` | counter | Each retry triggered by `RetryEmpty` | | `erpc_upstream_request_retries_total{reason="pending_tx"}` | counter | Each retry triggered by `RetryPending` | | `erpc_upstream_request_retries_total{reason="integrity_validation"}` | counter | Each retry due to a failed integrity check | | `erpc_network_consensus_rounds_total` | counter | Consensus rounds; `SkipConsensus=true` prevents increment | **Trace / log.** `X-ERPC-Force-Trace: true` (or `force-trace` query param) forces OTel sampler to record the span regardless of sampling rate (attribute `erpc.force_trace = true`). Source: [`common/tracing_util.go:89-100`](https://github.com/erpc/erpc/blob/main/common/tracing_util.go#L89-L100). `Request.Lock` / `Request.RLock` detail spans are emitted inside directive mutations ([`common/request.go:924-934`](https://github.com/erpc/erpc/blob/main/common/request.go#L924-L934)). At `trace` level, `applied request directives` is logged with `Interface("directives", ...)` after every `EnrichFromHttp` call. Source: [`erpc/http_server.go:659`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L659). ### Source code entry points - [`common/request.go:116-215`](https://github.com/erpc/erpc/blob/main/common/request.go#L116-L215) — `RequestDirectives` struct: all directive fields + library-only fields - [`common/request.go:563-676`](https://github.com/erpc/erpc/blob/main/common/request.go#L563-L676) — `ApplyDirectiveDefaults`: idempotency guard, nil-check per `*bool` field, copy-from-config - [`common/request.go:702-893`](https://github.com/erpc/erpc/blob/main/common/request.go#L702-L893) — `EnrichFromHttp`: fast-path scan, clone-on-write, all header + query parsers - [`common/config.go:2105-2175`](https://github.com/erpc/erpc/blob/main/common/config.go#L2105-L2175) — `DirectiveDefaultsConfig` struct; `skipCacheRead` custom YAML/JSON unmarshal - [`common/defaults.go:1454-1471`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1454-L1471) — `DirectiveDefaultsConfig.SetDefaults`: three `true` defaults (`enforceHighestBlock`, `enforceGetLogsBlockRange`, `enforceNonNullTaggedBlocks`) - [`erpc/http_server.go:1081-1272`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1081-L1272) — `executionHeadersMode`, `setResponseHeaders`, `writeCounterHeaders`, `writeResponseMetadataHeaders`, `writeUpstreamTraceHeaders` - [`erpc/network_executor.go:179-188`](https://github.com/erpc/erpc/blob/main/erpc/network_executor.go#L179-L188) — `SkipConsensus` branch - [`erpc/network_executor.go:374-462`](https://github.com/erpc/erpc/blob/main/erpc/network_executor.go#L374-L462) — `shouldRetryWithReason`: `RetryEmpty` / `RetryPending` checks and `EmptyResultMaxAttempts` cap - [`erpc/networks.go:374-441`](https://github.com/erpc/erpc/blob/main/erpc/networks.go#L374-L441) — selector-scoped served-tip: `requestSelector`, `servedTipPartitionFor`, partition cap - [`upstream/upstream.go:1505-1548`](https://github.com/erpc/erpc/blob/main/upstream/upstream.go#L1505-L1548) — `shouldSkip`: `UseUpstream` selector check via `UpstreamMatchesSelector` - [`common/matcher.go:34-112`](https://github.com/erpc/erpc/blob/main/common/matcher.go#L34-L112) — `WildcardMatch`, `MatchesSelector`, `UpstreamMatchesSelector` - [`erpc/skip_consensus_directive_test.go`](https://github.com/erpc/erpc/blob/main/erpc/skip_consensus_directive_test.go) — end-to-end `SkipConsensus` tests - [`common/request_test.go:391-541`](https://github.com/erpc/erpc/blob/main/common/request_test.go#L391-L541) — unit tests for boolean parse rules, header/query precedence, `ValidateTransactionsRoot` override ### Related pages - [Auth](/config/auth.llms.txt) — credential headers (`X-ERPC-Secret-Token`, `Authorization`, `X-Siwe-*`) parsed separately before directives. - [Rate limiters](/config/rate-limiters.llms.txt) — caps total request volume; important when `retryEmpty`/`retryPending` loops could multiply upstream calls. - [Consensus](/config/projects/consensus.llms.txt) — the branch that `skipConsensus` bypasses. - [Selection policies](/config/projects/selection-policies.llms.txt) — controls which upstreams are eligible before `useUpstream` further restricts the set. - [Matcher syntax](/config/matcher.llms.txt) — the WildcardMatch grammar used by `useUpstream` and `skipCacheRead` patterns. - [Survive provider outages](/use-cases/survive-provider-outages.llms.txt) — a use case that combines `retryEmpty`, `enforceHighestBlock`, and `useUpstream`. --- ## Navigation (machine-readable surface) - Up: [All pages index](https://docs.erpc.cloud/llms.txt) - Root index of every page: [llms.txt](https://docs.erpc.cloud/llms.txt) · everything in one file: [llms-full.txt](https://docs.erpc.cloud/llms-full.txt) ### Sibling pages - [Admin API](https://docs.erpc.cloud/operation/admin.llms.txt) — A built-in operator control plane — inspect topology, cordon sick upstreams without restarts, and manage API keys, all over a secure JSON-RPC 2.0 endpoint. - [Batching & multiplexing](https://docs.erpc.cloud/operation/batch.llms.txt) — Send one request, get back a merged response — eRPC parallelises inbound batch arrays, re-batches calls to supporting upstreams, and collapses identical in-flight requests so each unique call hits the network exactly once. - [CLI & env vars](https://docs.erpc.cloud/operation/cli.llms.txt) — Start, validate, or inspect your eRPC config from the command line — then deploy with confidence knowing exactly what the engine will run. - [Cordoning](https://docs.erpc.cloud/operation/cordoning.llms.txt) — Pull any upstream out of routing instantly with one admin call — no metric window to wait for, no config redeploy required. - [Healthcheck](https://docs.erpc.cloud/operation/healthcheck.llms.txt) — One endpoint that tells Kubernetes exactly when your pod is ready, draining, or broken — with eight probe strategies from "any upstream alive" to live chain-ID verification. - [Monitoring & metrics](https://docs.erpc.cloud/operation/monitoring.llms.txt) — Every subsystem in eRPC — upstreams, cache, rate limits, consensus, hedging — emits Prometheus metrics. One scrape target, full visibility, zero instrumentation work. - [Production checklist](https://docs.erpc.cloud/operation/production.llms.txt) — Go live confidently — a short list of settings that separate a hardened eRPC deployment from a dev-mode one. - [Tracing & logging](https://docs.erpc.cloud/operation/tracing.llms.txt) — Every request, cache lookup, and upstream call becomes a searchable span — shipped to any OTel backend. Secrets never leave the process. - [URL structure](https://docs.erpc.cloud/operation/url.llms.txt) — One URL pattern routes every chain — domain and network aliases let you publish clean, memorable endpoints without touching your app code.