# Server > Source: https://docs.erpc.cloud/config/server > eRPC's front door — dual-stack listeners, TLS/mTLS, a hard global timeout, gzip, drain-aware shutdown, and domain aliasing so any Host header routes to the right chain without touching a URL path. > Format: machine-readable markdown export of the docs page above. > All collapsible AI sections are inlined and fully expanded. # Server eRPC's `server:` block is your front door to every chain. One setting gives every deploy a clean drain window so zero requests drop during rollouts. Another collapses a sprawling URL scheme into bare hostnames like `eth.example.com`. Configure it once and stop worrying about dropped connections, spoofed IPs, or timeout mismatches between your LB and eRPC's own retry loop. **What you get** - Dual IPv4/IPv6 HTTP listeners with optional gRPC sharing on the same port - TLS and mutual TLS with a single extra field - Graceful two-phase drain so load balancers deplete before the process exits - Domain-based aliasing: map any `Host` header directly to a project and chain ## Quick taste Illustrative, not a tuned production config — minimal HTTP server with a 30-second ceiling: **Config path:** `server` **YAML — `erpc.yaml`:** ```yaml server: listenV4: true httpHostV4: "0.0.0.0" httpPortV4: 4000 # hard ceiling for the full request lifecycle — must be > sum of retry delays + hedge delay maxTimeout: 30s ``` **TypeScript — `erpc.ts`:** ```typescript import { createConfig } from "@erpc-cloud/config"; export default createConfig({ server: { listenV4: true, httpHostV4: "0.0.0.0", httpPortV4: 4000, // hard ceiling for the full request lifecycle — must be > sum of retry delays + hedge delay maxTimeout: "30s", }, }); ``` ## 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: configure server for production behind a load balancer** ```text Set up the eRPC server block in my eRPC config for production: behind an AWS ALB or Cloudflare, I need real client IPs extracted for per-IP rate limiting, a graceful drain window long enough for my LB health checks, and a maxTimeout that sits above my longest retry+hedge chain. Read the full reference first: https://docs.erpc.cloud/config/server.llms.txt ``` **Prompt Example #2: enable gRPC on a dedicated port without disrupting HTTP** ```text I want to enable the gRPC endpoint on eRPC but keep it on a separate port so HTTP traffic doesn't share the gRPC mux (and lose TimeoutHandler coverage). Update my eRPC config accordingly. Reference: https://docs.erpc.cloud/config/server.llms.txt ``` **Prompt Example #3: add mTLS so only my internal services can reach eRPC** ```text Lock down my eRPC instance so only services that present a certificate signed by my internal CA can connect. I only want to add the minimum fields needed — no unnecessary TLS knobs. Work with my existing eRPC config. Reference: https://docs.erpc.cloud/config/server.llms.txt ``` **Prompt Example #4: debug clients seeing wrong IPs or rate limits misfiring** ```text My per-IP rate limits are all firing against the load balancer address instead of the real client IP. Help me fix trustedIPForwarders and trustedIPHeaders in my eRPC config, and explain which headers are safe to trust given my proxy topology. Reference: https://docs.erpc.cloud/config/server.llms.txt ``` **Prompt Example #5: map subdomains to chains without changing URL paths** ```text I want eth.example.com and arb.example.com to route to Ethereum mainnet and Arbitrum without clients needing a /project/evm/1 path. Set up domain-based aliasing rules in my eRPC config. Reference: https://docs.erpc.cloud/config/server.llms.txt ``` --- ### Server — full agent reference ### How it works **Handler chain.** `NewHttpServer` composes the stack innermost to outermost: `createRequestHandler` → optional `gzipHandler` (response compression) → custom `TimeoutHandler` (global deadline = `maxTimeout`) → optionally an h2c/gRPC mux on IPv4 when gRPC shares the HTTP port. Two independent `http.Server` instances handle IPv4 and IPv6; only those whose `listenV4`/`listenV6` flag is true are created, and `Start()` fails if neither is set. gRPC sharing only ever applies to the IPv4 server. Source: [`erpc/http_server.go:L150-199`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L150-L199) **Timeout machinery.** eRPC does NOT use `net/http`'s built-in `TimeoutHandler`. Its own implementation buffers the entire response body in a pooled `bytes.Buffer` and stages headers privately; only when the inner handler finishes within the deadline does it flush to the real connection. On timeout: JSON-RPC `-32603` body at HTTP 200 (POST) or 504 (other). On client cancel: "request cancelled by client" at HTTP 200 (POST) or 503 with empty body (other). Source: [`erpc/http_timeout.go:L20-143`](https://github.com/erpc/erpc/blob/main/erpc/http_timeout.go#L20-L143) **Request and response gzip.** `enableGzip: true` wraps the handler in a `conditionalGzipWriter`. The first write decides once: if the chunk is smaller than 1024 bytes, no compression is applied; otherwise `Content-Length` is deleted, `Content-Encoding: gzip` + `Vary: Accept-Encoding` are set, and subsequent writes flow through a pooled `gzip.Writer`. Inbound gzip bodies (`Content-Encoding: gzip`) are always accepted and decompressed using a pooled reader, regardless of `enableGzip`. Source: [`erpc/http_server.go:L1685-1780`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1685-L1780) **gRPC port sharing.** When `grpcEnabled: true` and the gRPC v4 host:port equal the HTTP v4 host:port (the default derivation), the IPv4 HTTP handler multiplexes: HTTP/2 + `Content-Type: application/grpc` → in-process gRPC server, bypassing `TimeoutHandler` and `gzipHandler` entirely. Without TLS the combined handler is wrapped in h2c to accept cleartext HTTP/2. Sharing never applies to IPv6. To run a standalone gRPC server on a different port, set `grpcPortV4` to a different value. Source: [`erpc/grpc_server.go:L42-53`](https://github.com/erpc/erpc/blob/main/erpc/grpc_server.go#L42-L53) **TLS and mTLS.** When `tls.enabled` is true, both listeners use `ListenAndServeTLS` with TLS 1.2 as the minimum version, and gRPC uses TLS credentials. Setting `caFile` is the only knob needed to enable mTLS: it populates `ClientCAs` and sets `ClientAuth = RequireAndVerifyClientCert`, forcing every client to present a valid certificate. Source: [`erpc/http_server.go:L1537-1637`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1537-L1637) **Trusted-proxy IP extraction.** `resolveRealClientIP` trusts forwarding headers only when the direct peer is inside `trustedIPForwarders` (default: loopback only). It walks `trustedIPHeaders` in order, parses each value XFF-style, strips trailing trusted-proxy entries right-to-left, and returns the nearest untrusted hop. If every hop is trusted, it falls back to the direct peer IP. RFC 7239 `Forwarded` is not supported. Source: [`erpc/http_server.go:L1782-1905`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1782-L1905) **Domain-based aliasing.** `server.aliasing.rules[]` maps a request `Host` header to a pre-selected `(project, architecture, chain)` so callers can use a bare URL like `https://eth.example.com`. Rules are evaluated in order; first wildcard match wins. Not every combination of `serveProject`/`serveArchitecture`/`serveChain` is valid — see Edge cases. Source: [`erpc/http_server.go:L232-257`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L232-L257) **Graceful shutdown.** Two goroutines watch the app context on SIGTERM: one flips a `draining` flag immediately so `GET /healthcheck` returns 503 "shutting down" (letting the LB drain traffic); after sleeping `waitBeforeShutdown` (default `10s`), `http.Server.Shutdown` is called with a hardcoded 30s budget. After the context is done, `Init` sleeps an additional `waitAfterShutdown` (default `10s`) before process exit, letting telemetry exporters flush. Source: [`erpc/http_server.go:L209-221`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L209-L221) ### Config schema All fields live under `server.` unless noted. `Duration` accepts Go duration strings (`"30s"`) or bare integers interpreted as **milliseconds** (`"150"` = 150 ms, not 150 s). Struct at [`common/config.go:L130-200`](https://github.com/erpc/erpc/blob/main/common/config.go#L130-L200). | Field | Type | Default | Behavior / footguns | |---|---|---|---| | `server.listenV4` | `*bool` | `true` (except under `go test`) | Enables the IPv4 `http.Server`. Validation requires `httpHostV4` + `httpPortV4` when true. [`common/defaults.go:L640-644`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L640-L644) | | `server.httpHostV4` | `*string` | `"0.0.0.0"` | IPv4 bind host. [`common/defaults.go:L645-647`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L645-L647) | | `server.httpPortV4` | `*int` | `4000` | IPv4 HTTP port. Startup fails with "server.httpPortV4 is not configured" if nil while `listenV4` is true. [`common/defaults.go:L655-657`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L655-L657) | | `server.listenV6` | `*bool` | unset (OFF) | IPv6 must be opted in; `SetDefaults` never sets it. [`erpc/http_server.go:L191`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L191) | | `server.httpHostV6` | `*string` | `"[::]"` | IPv6 bind host. [`common/defaults.go:L648-650`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L648-L650) | | `server.httpPortV6` | `*int` | `httpPortV4 + 1000` (default `5000`) | Derived to avoid collision with the default metrics port 4001. [`common/defaults.go:L658-668`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L658-L668) | | `server.httpPort` | `*int` | deprecated alias of `httpPortV4` | If set and `httpPortV4` is unset, `httpPortV4` is assigned its value. After `SetDefaults` both always agree. No warning is emitted. New configs must use `httpPortV4`. [`common/defaults.go:L652-661`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L652-L661) | | `server.grpcEnabled` | `*bool` | `false` | Enables gRPC. With default port derivation, gRPC shares the HTTP v4 port (in-handler mux + h2c). [`common/defaults.go:L669-671`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L669-L671) | | `server.grpcHostV4` | `*string` | copy of `httpHostV4` | [`common/defaults.go:L672-675`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L672-L675) | | `server.grpcPortV4` | `*int` | copy of `httpPortV4` | Equal to HTTP by default — triggers port sharing. Set a different value for a standalone gRPC server. [`common/defaults.go:L676-679`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L676-L679) | | `server.grpcHostV6` | `*string` | copy of `httpHostV6` | No IPv6 port-sharing logic exists. [`common/defaults.go:L680-683`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L680-L683) | | `server.grpcPortV6` | `*int` | copy of `httpPortV6` | [`common/defaults.go:L684-687`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L684-L687) | | `server.grpcMaxRecvMsgSize` | `*int` | `104857600` (100 MiB) | gRPC max receive message size. [`common/defaults.go:L688-690`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L688-L690) | | `server.grpcMaxSendMsgSize` | `*int` | `104857600` (100 MiB) | gRPC max send message size. [`common/defaults.go:L691-693`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L691-L693) | | `server.maxTimeout` | `*Duration` | `150s` | Global per-HTTP-request deadline. **Required non-zero** — validation fails with "server.maxTimeout is required" if absent or zero. Bare integer YAML values are milliseconds: `maxTimeout: 150` = 150 ms. [`common/defaults.go:L694-697`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L694-L697) | | `server.readTimeout` | `*Duration` | `30s` | `http.Server.ReadTimeout` — covers reading headers and body. [`common/defaults.go:L698-701`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L698-L701) | | `server.writeTimeout` | `*Duration` | `120s` | `http.Server.WriteTimeout` — covers writing the response. The entire response is buffered by `TimeoutHandler` before reaching the socket, so this only matters at final flush. [`common/defaults.go:L702-705`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L702-L705) | | `server.enableGzip` | `*bool` | `true` | Wraps handler in `gzipHandler` for response compression. Inbound gzip is always accepted regardless of this flag. [`common/defaults.go:L706-708`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L706-L708) | | `server.tls.enabled` | `bool` | `false` | When true, both listeners use `ListenAndServeTLS` with TLS 1.2 minimum; gRPC also uses TLS. Disables h2c on the shared port. [`erpc/http_server.go:L1537-1554`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1537-L1554) | | `server.tls.certFile` | `string` | `""` | PEM cert path. Load failure → "failed to load TLS certificate and key". | | `server.tls.keyFile` | `string` | `""` | PEM key path. | | `server.tls.caFile` | `string` | `""` | When set, enables **mandatory mTLS**: `ClientAuth = RequireAndVerifyClientCert`. All clients must present a valid cert. Setting this field alone is sufficient — no other change needed. [`erpc/http_server.go:L1621-1633`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1621-L1633) | | `server.tls.insecureSkipVerify` | `bool` | `false` | **No effect on inbound TLS.** Go's TLS stack ignores this in server mode. Inbound cert verification is controlled only by `caFile`. This field only matters when the same struct is reused for outbound connections (Redis, tracing exporters). [`erpc/http_server.go:L1635`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1635) | | `server.aliasing.rules` | `[]*AliasingRuleConfig` | `nil` (auto-injected when zero projects) | Evaluated per request against `Host` (port stripped) in order; first wildcard match wins. [`erpc/http_server.go:L232-257`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L232-L257) | | `server.aliasing.rules[].matchDomain` | `string` | — | Wildcard/boolean pattern (`*`, `\|`, `&`, `!`, parens) matched against the request host. [`common/matcher.go:L34-47`](https://github.com/erpc/erpc/blob/main/common/matcher.go#L34-L47) | | `server.aliasing.rules[].serveProject` | `string` | `""` | Pre-selected project ID. | | `server.aliasing.rules[].serveArchitecture` | `string` | `""` | Pre-selected architecture. Without `serveProject`, requires project in the URL path. Combining with `serveChain` but without `serveProject` is always `ErrInvalidUrlPath`. | | `server.aliasing.rules[].serveChain` | `string` | `""` | Pre-selected chainId. Combining `serveProject` + `serveChain` without `serveArchitecture` is rejected at request time. | | `server.waitBeforeShutdown` | `*Duration` | `10s` | Sleep after app-ctx cancel before calling `http.Server.Shutdown`. Lets readiness probes fail so the LB drains traffic first. [`common/defaults.go:L709-712`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L709-L712) | | `server.waitAfterShutdown` | `*Duration` | `10s` | Sleep in `Init` after both listeners stop, before process exit. Lets telemetry exporters flush. [`common/defaults.go:L713-716`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L713-L716) | | `server.includeErrorDetails` | `*bool` | `true` | When true, error responses include `error.data` = full original error object, except for `eth_call` (clients expect string revert data). Many early/client-side error paths force `true` regardless of this flag. [`common/defaults.go:L717-719`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L717-L719) | | `server.trustedIPForwarders` | `[]string` | `["127.0.0.1/8", "::1/128"]` | IPs/CIDRs of proxies whose forwarding headers are trusted. Invalid entries are warned and ignored at runtime. [`common/defaults.go:L725-729`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L725-L729) | | `server.trustedIPHeaders` | `[]string` | `[]` (none trusted by default) | Header names parsed XFF-style for real client IP, only when the direct peer is a trusted forwarder. RFC 7239 `Forwarded` is not supported. [`common/defaults.go:L730-733`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L730-L733) | | `server.responseHeaders` | `map[string]string` | `nil` | Static headers added to every response. Values are env-expanded once at startup (`${VAR}` and `$VAR`). **Footgun:** headers whose value expands to empty string are silently dropped with only a Debug log — no warning, no error. [`erpc/http_server.go:L135-148`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L135-L148) | | `server.executionHeaders` | `*ExecutionHeadersMode` | `"all"` | `"all"` = counters + metadata + per-attempt `X-ERPC-Upstreams` log; `"summary"` = counters + metadata; `"off"` = no `X-ERPC-*` diagnostic headers. [`common/defaults.go:L720-723`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L720-L723) | | `healthCheck.mode` | `HealthCheckMode` | `"networks"` | `"simple"` = plain `OK` text; `"networks"` = per-network JSON; `"verbose"` = full JSON. [`erpc/healthcheck.go:L337-396`](https://github.com/erpc/erpc/blob/main/erpc/healthcheck.go#L337-L396) | | `healthCheck.auth` | `*AuthConfig` | `nil` | When set, a dedicated auth registry guards healthcheck endpoints. [`erpc/http_server.go:L201-207`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L201-L207) | | `healthCheck.defaultEval` | `string` | `"any:initializedUpstreams"` | Default eval strategy when `?eval=` query param is absent. [`erpc/healthcheck.go:L106-112`](https://github.com/erpc/erpc/blob/main/erpc/healthcheck.go#L106-L112) | | `projects[].cors.allowedOrigins` | `[]string` | `["*"]` | Wildcard-matched against `Origin` header. Configured per-project (and on admin) but enforced by the HTTP server. [`erpc/http_server.go:L1022-1033`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1022-L1033) | | `cors.allowedMethods` | `[]string` | `["GET","POST","OPTIONS"]` | Joined into `Access-Control-Allow-Methods`. [`erpc/http_server.go:L1055`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1055) | | `cors.allowedHeaders` | `[]string` | `["content-type","authorization","x-erpc-secret-token"]` | Joined into `Access-Control-Allow-Headers`. [`erpc/http_server.go:L1056`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1056) | | `cors.exposedHeaders` | `[]string` | `nil` | Joined into `Access-Control-Expose-Headers`. [`erpc/http_server.go:L1057`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1057) | | `cors.allowCredentials` | `*bool` | `false` | Emits `Access-Control-Allow-Credentials: true` only when true. [`erpc/http_server.go:L1059-1061`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1059-L1061) | | `cors.maxAge` | `int` | `3600` | Emits `Access-Control-Max-Age` when > 0. [`erpc/http_server.go:L1063-1065`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1063-L1065) | **Hardcoded constants (non-configurable):** - `IdleTimeout` = 300s on both servers - `MaxHeaderBytes` = 1 MiB — the only hard inbound size limit - No request-body size limit — `util.ReadAll` copies until EOF with no upper bound; effective limits are `readTimeout` and process memory - Graceful shutdown budget = 30s - Response compression threshold = 1024 bytes on the first write - TLS `MinVersion` = TLS 1.2 ### Worked examples All patterns below are distilled from real production fleets; comments explain the non-obvious choices. **1. Public edge gateway behind an Anycast load balancer.** Many PaaS proxies inject the real client IP into a platform header (e.g. `Fly-Client-IP`, `CF-Connecting-IP`); the RFC-1918 trusted-forwarder CIDRs cover the platform mesh. `includeErrorDetails: false` strips internal stack traces from public responses. `waitBeforeShutdown: 30s` covers a couple of LB health-check intervals so rolling deploys drain fully before shutdown: **Config path:** `server` **YAML — `erpc.yaml`:** ```yaml server: # platform machines typically sit in RFC-1918 space; trust private CIDRs trustedIPForwarders: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 # your platform injects the real client IP here — prefer it over spoofable X-Forwarded-For trustedIPHeaders: - CF-Connecting-IP # strip internal error details from public 5xx responses includeErrorDetails: false # 30s covers a couple of LB health-check intervals before Shutdown is called waitBeforeShutdown: 30s # keep the process alive long enough for telemetry exporters to flush waitAfterShutdown: 30s responseHeaders: # expose which region handled this request — useful for latency debugging X-ERPC-Region: \${REGION} X-ERPC-Machine: \${MACHINE_ID} ``` **TypeScript — `erpc.ts`:** ```typescript import { createConfig } from "@erpc-cloud/config"; export default createConfig({ server: { // platform machines typically sit in RFC-1918 space; trust private CIDRs trustedIPForwarders: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], // your platform injects the real client IP here — prefer it over spoofable X-Forwarded-For trustedIPHeaders: ["CF-Connecting-IP"], // strip internal error details from public 5xx responses includeErrorDetails: false, // 30s covers a couple of LB health-check intervals before Shutdown is called waitBeforeShutdown: "30s", // keep the process alive long enough for telemetry exporters to flush waitAfterShutdown: "30s", responseHeaders: { // expose which region handled this request — useful for latency debugging "X-ERPC-Region": "\${REGION}", "X-ERPC-Machine": "\${MACHINE_ID}", }, }, }); ``` **2. Internal Kubernetes deployment with gRPC shared on the HTTP port.** Internal indexing fleets enable gRPC so indexer workers can use the gRPC connector without a separate endpoint. The default port derivation (`grpcPortV4` = `httpPortV4`) activates the in-process h2c mux — no separate `grpcPortV4` needed. Note that gRPC traffic on the shared port bypasses `TimeoutHandler` and gzip: **Config path:** `server` **YAML — `erpc.yaml`:** ```yaml server: # gRPC shares port 4000 via the h2c in-process mux # (grpcPortV4 defaults to httpPortV4 — no explicit value needed) grpcEnabled: true # 30s matches two readiness-probe intervals in most K8s LB configs waitBeforeShutdown: 30s waitAfterShutdown: 30s ``` **TypeScript — `erpc.ts`:** ```typescript import { createConfig } from "@erpc-cloud/config"; export default createConfig({ server: { // gRPC shares port 4000 via the h2c in-process mux // (grpcPortV4 defaults to httpPortV4 — no explicit value needed) grpcEnabled: true, // 30s matches two readiness-probe intervals in most K8s LB configs waitBeforeShutdown: "30s", waitAfterShutdown: "30s", }, }); ``` **3. Standalone gRPC on a dedicated port (when TimeoutHandler must cover all traffic).** When `grpcPortV4` differs from `httpPortV4`, eRPC starts a fully independent gRPC server instead of muxing. HTTP requests flow through `TimeoutHandler` and gzip as normal; gRPC has its own listener. Use this when you need `maxTimeout` enforcement on HTTP while still serving gRPC: **Config path:** `server` **YAML — `erpc.yaml`:** ```yaml server: httpPortV4: 4000 grpcEnabled: true # different port → standalone gRPC server, HTTP keeps its TimeoutHandler grpcPortV4: 4010 ``` **TypeScript — `erpc.ts`:** ```typescript import { createConfig } from "@erpc-cloud/config"; export default createConfig({ server: { httpPortV4: 4000, grpcEnabled: true, // different port → standalone gRPC server, HTTP keeps its TimeoutHandler grpcPortV4: 4010, }, }); ``` **4. Mutual TLS for a private RPC gateway.** Adding `caFile` is the only field needed to flip mTLS on — it sets `ClientAuth = RequireAndVerifyClientCert` automatically. All clients must present a valid certificate signed by that CA. `waitBeforeShutdown: 20s` is two readiness-probe intervals for an NLB (10s probe interval × 2): **Config path:** `server` **YAML — `erpc.yaml`:** ```yaml server: httpPortV4: 4443 tls: enabled: true certFile: /etc/erpc/tls.crt keyFile: /etc/erpc/tls.key # setting caFile alone enables mTLS — no other field required caFile: /etc/erpc/ca.crt # NLB probe interval is typically 10s; 2× = full drain window waitBeforeShutdown: 20s waitAfterShutdown: 5s ``` **TypeScript — `erpc.ts`:** ```typescript import { createConfig } from "@erpc-cloud/config"; export default createConfig({ server: { httpPortV4: 4443, tls: { enabled: true, certFile: "/etc/erpc/tls.crt", keyFile: "/etc/erpc/tls.key", // setting caFile alone enables mTLS — no other field required caFile: "/etc/erpc/ca.crt", }, // NLB probe interval is typically 10s; 2× = full drain window waitBeforeShutdown: "20s", waitAfterShutdown: "5s", }, }); ``` **5. Domain-based aliasing for a multi-chain gateway.** Operators serving Ethereum mainnet and Arbitrum on separate subdomains can pre-route requests by `Host` header so callers use bare URLs like `https://eth.example.com`. Specific rules are placed before the wildcard catch-all — first match wins. Combining `serveProject` + `serveChain` without `serveArchitecture` is an error; all three must be present for fully resolved routing: **Config path:** `server.aliasing` **YAML — `erpc.yaml`:** ```yaml server: aliasing: rules: # specific rules must come before the wildcard — first match wins - matchDomain: "eth.example.com" serveProject: main serveArchitecture: evm serveChain: "1" - matchDomain: "arb.example.com" serveProject: main serveArchitecture: evm serveChain: "42161" # wildcard catch-all: project pre-selected, chain still from URL path - matchDomain: "*.example.com" serveProject: main ``` **TypeScript — `erpc.ts`:** ```typescript import { createConfig } from "@erpc-cloud/config"; export default createConfig({ server: { aliasing: { rules: [ // specific rules must come before the wildcard — first match wins { matchDomain: "eth.example.com", serveProject: "main", serveArchitecture: "evm", serveChain: "1" }, { matchDomain: "arb.example.com", serveProject: "main", serveArchitecture: "evm", serveChain: "42161" }, // wildcard catch-all: project pre-selected, chain still from URL path { matchDomain: "*.example.com", serveProject: "main" }, ], }, }, }); ``` ### Request/response behavior **HTTP status mapping** (`determineResponseStatusCode` and `handleErrorResponse`, both switch on the same error code set): | Status | Conditions | |--------|------------| | 400 | `ErrInvalidUrlPath`, `ErrJsonRpcRequestUnmarshal`, `ErrInvalidRequest` | | 401 | `ErrAuthUnauthorized`, `ErrEndpointUnauthorized` | | 404 | `ErrProjectNotFound`, `ErrNetworkNotFound`, `ErrNetworkNotSupported` | | 429 | `ErrAuthRateLimitRuleExceeded`, `ErrProjectRateLimitRuleExceeded`, `ErrNetworkRateLimitRuleExceeded`, `ErrEndpointCapacityExceeded` | | 200 | everything else; JSON-RPC application errors always stay 200 | Note: `writeFatalError` (panics, write failures) and the timeout/cancel paths force HTTP 200 for POST regardless of error type. Source: [`erpc/http_server.go:L1280-1317`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1280-L1317) **Error response shapes:** - Standard JSON-RPC single error: `{"jsonrpc":"","id":,"error":{"code":,"message":""[,"data":]}}` — `includeErrorDetails` adds `error.data` except on `eth_call` - Timeout body: `{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"http request handling timeout"}}` at HTTP 200 (POST) / 504 (other) [[`erpc/http_timeout.go:L107-122`](https://github.com/erpc/erpc/blob/main/erpc/http_timeout.go#L107-L122)] - Client-cancel body: `{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"request cancelled by client"}}` at HTTP 200 (POST) / 503 (other) - Fatal-path body (panic/write failure): `{"jsonrpc":"2.0","error":{"code":-32603,"message":...}}` — **no `id` field** - Batch responses are always HTTP 200 and carry no `X-ERPC-*` diagnostic headers; each element is written as a streaming JSON array. Source: [`erpc/http_server.go:L703-720`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L703-L720) **Diagnostic response headers** (single responses only; `setResponseHeaders`): - `X-ERPC-Version`, `X-ERPC-Commit` — emitted on every response (build-time ldflags vars, defaults `"dev"` and `"none"`). [`erpc/http_server.go:L260-261`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L260-L261) - `X-ERPC-Attempts`, `X-ERPC-Upstream-Attempts/-Retries/-Hedges`, `X-ERPC-Network-Attempts/-Retries/-Hedges` — counters, **zero-filled on early errors** (not omitted; only `mode: off` removes them). [`erpc/http_server.go:L1149-1166`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1149-L1166) - `X-ERPC-Cache-Attempts/-Retries/-Hedges` — emitted only when the cache scope was exercised. [`erpc/http_server.go:L1170-1174`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1170-L1174) - `X-ERPC-Consensus-Slots`, `X-ERPC-Consensus-Disputes`, `X-ERPC-Consensus-Low-Participants` — emitted only when consensus was exercised and the count is > 0. [`erpc/http_server.go:L1175-1183`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1175-L1183) - `X-ERPC-Cache: HIT|MISS`, `X-ERPC-Upstream`, `X-ERPC-Duration` — response metadata - `X-ERPC-Upstreams` (mode `all` only): per-attempt participation log in the form `=::ms[:won]` - `traceparent` (+ `tracestate`) — injected when tracing is enabled. [`common/tracing_util.go:L58-65`](https://github.com/erpc/erpc/blob/main/common/tracing_util.go#L58-L65) **Request headers with server-level effects:** - `Host` — aliasing match (port stripped) - `Content-Encoding: gzip` — inbound body decompression (always, regardless of `enableGzip`) - `Accept-Encoding: gzip` — response compression eligibility - `Content-Type: application/grpc` over HTTP/2 — gRPC mux on shared port (bypasses `TimeoutHandler` and gzip) - `Origin` — CORS evaluation; absent Origin bypasses CORS entirely - `X-ERPC-Force-Trace: true|1|yes` or `?force-trace=true|1|yes` — force-sample the OTel trace for this request. [`common/tracing_util.go:L106-119`](https://github.com/erpc/erpc/blob/main/common/tracing_util.go#L106-L119) - `traceparent` / `tracestate` — W3C trace-context extraction on ingress. [`common/tracing_util.go:L67-73`](https://github.com/erpc/erpc/blob/main/common/tracing_util.go#L67-L73) - `?eval=` — healthcheck strategy override (bypasses `healthCheck.defaultEval`). [`erpc/healthcheck.go:L106-112`](https://github.com/erpc/erpc/blob/main/erpc/healthcheck.go#L106-L112) - Auth headers: `?token=` (deprecated), `X-ERPC-Secret-Token`, `Authorization: Basic|Bearer`, `?secret=`, `?jwt=`, `?signature=&message=`, `X-Siwe-Message`/`X-Siwe-Signature`. [`auth/http.go:L13`](https://github.com/erpc/erpc/blob/main/auth/http.go#L13) ### Best practices - Set `maxTimeout` as a Go duration string (`"30s"`), never a bare integer. `maxTimeout: 150` means 150 milliseconds, not 150 seconds — this is the most common misconfiguration footgun. - Size `maxTimeout` to be greater than your largest network-level `failsafe.timeout` plus total retry overhead, or retries will be cut short by the server ceiling before they complete. See [Timeout](/config/failsafe/timeout.llms.txt). - In production behind an LB, set `waitBeforeShutdown` to at least two readiness-probe intervals (often 20s). The default `10s` is too short for many Kubernetes setups. - **Never omit `trustedIPForwarders`** when running behind a proxy. Without it, every request appears to come from the LB IP, breaking per-IP rate limits, auth network strategies, and client identity logging. - Setting `tls.caFile` enables mTLS implicitly and immediately — all clients must then present a certificate. Verify this is intended before adding the field in production. - Keep `executionHeaders: "summary"` or `"off"` for production traffic if response size is sensitive — `"all"` mode emits a verbose `X-ERPC-Upstreams` header that grows linearly with the number of upstream attempts. - Use `responseHeaders` for static CORS or security headers, but pin the env vars at deploy time. Values that expand to empty are silently dropped with no error, so a missing env var makes the header vanish invisibly. ### Edge cases & gotchas 1. **`maxTimeout` bare integer is milliseconds.** `maxTimeout: 150` = 150 ms, not 150 s. Always use a duration string like `"150s"`. Source: [`common/duration.go:L20-31`](https://github.com/erpc/erpc/blob/main/common/duration.go#L20-L31) 2. **`grpcEnabled: true` with default ports silently shares the HTTP v4 port.** gRPC traffic bypasses `maxTimeout` and gzip. Source: [`erpc/http_server.go:L161-177`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L161-L177) 3. **mTLS is implicit.** Setting `tls.caFile` flips `ClientAuth = RequireAndVerifyClientCert` — all clients must present certs. No other field is needed. Source: [`erpc/http_server.go:L1621-1633`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1621-L1633) 4. **`tls.insecureSkipVerify` does nothing for inbound connections.** It is a client-dialing flag only; Go's TLS server ignores it. Inbound cert verification is controlled by `caFile` alone. Source: [`erpc/http_server.go:L1635`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1635) 5. **`responseHeaders` env vars that expand to empty are silently dropped.** No warning is emitted — only a Debug log. A missing env var in production silently removes the header from all responses. Source: [`erpc/http_server.go:L135-148`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L135-L148) 6. **`listenV6` has no default.** IPv6 is OFF unless explicitly set to `true`; it is never auto-enabled. Source: [`common/defaults.go:L640-650`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L640-L650) 7. **`httpPort` (deprecated) is silently migrated.** No warning is emitted. After `SetDefaults`, `httpPort` always equals `httpPortV4`. New configs must use `httpPortV4`. Source: [`common/defaults.go:L652-661`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L652-L661) 8. **No request-body size limit.** There is no `http.MaxBytesReader`; a multi-GB body is fully read into memory. Only `readTimeout` and process memory provide back-pressure. Source: [`util/reader.go:L12-32`](https://github.com/erpc/erpc/blob/main/util/reader.go#L12-L32) 9. **POST requests can still get non-200 status codes.** HTTP 400/401/404/429 mappings apply to POST; only the fatal-writer and timeout/cancel paths force 200-for-POST. Source: [`erpc/http_server.go:L1470-1491`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1470-L1491) 10. **Whole responses are buffered before hitting the socket.** Large responses consume RAM; pooled buffers above 256 KiB are discarded to GC. `writeTimeout` only matters at the final socket copy. Source: [`erpc/http_timeout.go:L43-48`](https://github.com/erpc/erpc/blob/main/erpc/http_timeout.go#L43-L48) 11. **Disallowed CORS origins are not blocked for non-OPTIONS requests.** The request proceeds without `Access-Control-*` headers; the browser enforces the block. Source: [`erpc/http_server.go:L1036-1051`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1036-L1051) 12. **Zero-project configs self-alias.** A default `main` project and `matchDomain: "*"` rule are injected so `/evm/1` works without a project segment. Source: [`common/defaults.go:L100-111`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L100-L111) 13. **`serveProject` + `serveChain` without `serveArchitecture` is always an error.** Source: [`erpc/http_server.go:L985-990`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L985-L990) 14. **Full request bodies are logged at Info level by default.** Sensitive params (private keys, tokens in JSON-RPC args) appear in logs. Source: [`erpc/http_server.go:L398-402`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L398-L402) 15. **`X-ERPC-Attempts` excludes NetworkAttempts** to avoid double-counting rotations. Source: [`erpc/http_server.go:L1151-1157`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1151-L1157) 16. **Batch detection is byte-exact.** Leading whitespace before `[` makes the body parse as a single (invalid) request. Source: [`erpc/http_server.go:L405`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L405) 17. **Any non-POST/non-OPTIONS request becomes a healthcheck**, including `GET /myproject/evm/1`. Source: [`erpc/http_server.go:L1001-1003`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1001-L1003) 18. **RFC 7239 `Forwarded` is dead code** — only headers named in `trustedIPHeaders` are consulted, parsed XFF-style. Source: [`erpc/http_server.go:L1856-1881`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1856-L1881) 19. **CORS metric label mismatch.** The label named `project` is populated with the URL path, not the project ID. Source: [`erpc/http_server.go:L1020`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1020) 20. **`serveArchitecture`-only aliasing requires a project in the URL path.** Combining `serveArchitecture` + `serveChain` without `serveProject` is always `ErrInvalidUrlPath`. Source: [`erpc/http_server.go:L963-990`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L963-L990) 21. **OPTIONS to a project without CORS config falls through to normal request handling.** The OPTIONS early-return lives inside the `CORS != nil` branch; a project with no `cors:` block sends an empty body OPTIONS through the full JSON-RPC path, returning a JSON-RPC error (not a 204). Source: [`erpc/http_server.go:L343-347`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L343-L347) 22. **Gzip decision is first-write-only.** A response whose first chunk is smaller than 1024 bytes is never compressed, even if subsequent writes are large. Source: [`erpc/http_server.go:L1708-1717`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1708-L1717) 23. **Done/canceled race silently drops the response.** If the inner handler finishes but the request context has already errored (e.g. a race between handler return and deadline), nothing is written to the socket. The timeout layer has already responded. Source: [`erpc/http_timeout.go:L72-80`](https://github.com/erpc/erpc/blob/main/erpc/http_timeout.go#L72-L80) 24. **Counter headers are zero-filled on early errors, not omitted.** Even URL-parse or project-lookup failures emit `X-ERPC-Attempts: 0`, `X-ERPC-Upstream-Attempts: 0`, etc., because `writeCounterHeaders` runs against a nil-safe snapshot. Only `executionHeaders: "off"` suppresses them entirely. Source: [`erpc/http_server.go:L1149-1166`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1149-L1166) 25. **`ErrUnknown` fallback body is not JSON-RPC shaped.** When `processErrorBody` receives an error that survives all unwrapping as neither a `*common.BaseError` nor `common.StandardError`, it produces `{"code":"ErrUnknown","message":"unexpected server error","cause":{...}}` — a struct dump, not `{"jsonrpc":"2.0","error":{...}}`. Clients parsing `response.error.code` as an integer will fail; detection must branch on whether the outer object has a `code` string key vs an `error` object key. Source: [`erpc/http_server.go:L1450-1454`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1450-L1454) 26. **`ErrorStatusCode()` on error types is dead code.** Every error type in `common/errors.go` implements `ErrorStatusCode() int`, but there are no call sites. The wire HTTP status is determined exclusively by the two switch blocks in `determineResponseStatusCode` and `handleErrorResponse` using `common.HasErrorCode`. Reading an error type's `ErrorStatusCode()` to infer the wire status gives wrong answers for many types (e.g. `ErrNetworkInitializing` → 503, `ErrUpstreamRateLimitRuleExceeded` → 429 per the method, but neither appears in the switch). Source: [`erpc/http_server.go:L1280-1317`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1280-L1317) 27. **Sonic encoder writes a trailing newline and disables HTML escaping globally.** The early-error path uses `encoder.Encode` (trailing `\n`) and sonic's HTML escaping is off (`common/sonic.go`). JSON field values such as URLs are not HTML-escaped in error bodies. Source: [`erpc/http_server.go:L229-230`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L229-L230) ### Observability | Metric | Type | Labels | When it fires | |---|---|---|---| | `erpc_unexpected_panic_total` | counter | `scope`, `extra`, `error` | Scopes: `request-handler`, `final-error-writer`, `top-level-handler`, `timeout-handler`, `validate-pattern` | | `erpc_cors_requests_total` | counter | `project` (= URL path), `origin` | Every request carrying an `Origin` header | | `erpc_cors_preflight_requests_total` | counter | `project`, `origin` | Allowed-origin OPTIONS preflight requests | | `erpc_cors_disallowed_origin_total` | counter | `project`, `origin` | Origin matched no allowlist entry | **Trace spans:** `Http.ReceivedRequest` (SpanKind=server; attrs `http.method`, `http.url`, `http.scheme`, `http.user_agent`); `Request.Handle` per sub-request; detail spans `Http.ReadBody`, `Http.ParseRequests`, `HttpServer.WriteResponse`; `w3c traceparent`/`tracestate` extraction on ingress, injection on egress. **Note:** there is no generic `http_requests_total`-style server metric; request accounting happens at the network/upstream layers. **Notable log lines:** - `"received http request"` at **Info** — includes the full raw JSON body; sensitive params appear at default log level - `"entering draining mode → healthcheck will fail"` on SIGTERM - `"starting IPv4/IPv6 HTTP server on "` at listen - `"TLS enabled for IPv4/IPv6 HTTP server"` when TLS is active - `"http server forced to shutdown"` / `"http server stopped"` on graceful drain completion - `"custom response header configured"` / `"custom response header skipped (empty value after env expansion)"` for `responseHeaders` - `"invalid CIDR/IP in trusted forwarders; ignoring"` for malformed forwarder entries - `"CORS request from disallowed origin..."` at Debug when an origin is rejected ### Source code entry points - [`erpc/http_server.go:L1-L230`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1-L230) — `NewHttpServer`: handler chain assembly, gzip/timeout wrapping, gRPC mux, IPv4/IPv6 server construction, `responseHeaders` env expansion, trusted forwarder parsing - [`common/defaults.go:L640-L733`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L640-L733) — `ServerConfig.SetDefaults`: all defaults including port derivation, deprecated `httpPort` migration, zero-project aliasing injection - [`erpc/http_timeout.go:L20-L143`](https://github.com/erpc/erpc/blob/main/erpc/http_timeout.go#L20-L143) — custom `TimeoutHandler`: buffered response, timeout/cancel body shapes, panic propagation - [`erpc/http_server.go:L1537-L1637`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1537-L1637) — TLS config construction, `ListenAndServeTLS`, mTLS `ClientAuth` assignment - [`erpc/http_server.go:L1782-L1905`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1782-L1905) — `resolveRealClientIP`: trusted forwarder check, XFF right-trim, fallback to peer IP - [`erpc/http_server.go:L810-L1006`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L810-L1006) — `parseUrlPath`: full aliasing × path-segment case matrix - [`common/validation.go:L70-L109`](https://github.com/erpc/erpc/blob/main/common/validation.go#L70-L109) — `ServerConfig.Validate`: listen host/port pairing, `maxTimeout` required, forwarder IP/CIDR syntax - [`erpc/http_server_test.go`](https://github.com/erpc/erpc/blob/main/erpc/http_server_test.go) — path parsing matrix, CORS, timeouts, aliasing fixtures ### Related pages - [Timeout](/config/failsafe/timeout.llms.txt) — per-network timeout that must stay below `server.maxTimeout` or the server ceiling fires first. - [Rate limiters](/config/rate-limiters.llms.txt) — per-IP rate limits depend on accurate client IP, which requires correct `trustedIPForwarders` + `trustedIPHeaders`. - [Auth](/config/auth.llms.txt) — authentication strategies consume the resolved client IP from this layer. - [URL structure & aliasing](/operation/url.llms.txt) — full path-parsing logic and all valid segment combinations for domain aliasing. - [Deployment overview](/deployment/self-hosted.llms.txt) — where `server:` fits in a production Kubernetes or Docker deployment. --- ## 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 - [Authentication](https://docs.erpc.cloud/config/auth.llms.txt) — Lock down every request with a token, JWT, wallet signature, or IP allowlist — and bind each identity to its own rate-limit budget. - [Example config](https://docs.erpc.cloud/config/example.llms.txt) — A production-ready starting point you can copy today, plus a complete annotated reference of every config section — caching, failover, hedging, rate limits, and observability included. - [Failsafe](https://docs.erpc.cloud/config/failsafe.llms.txt) — Six composable failsafe policies that keep every RPC request succeeding — even when upstreams are slow, wrong, or temporarily down. - [Matcher syntax](https://docs.erpc.cloud/config/matcher.llms.txt) — One pattern engine everywhere — globs, boolean logic, and hex ranges that work identically across cache policies, failsafe rules, rate limits, method filters, and routing directives. - [Projects](https://docs.erpc.cloud/config/projects.llms.txt) — One eRPC, many tenants — each project gets its own networks, upstreams, auth, and budgets. - [Rate Limiters](https://docs.erpc.cloud/config/rate-limiters.llms.txt) — Stop a runaway caller or a misbehaving provider from affecting everyone else — eRPC applies independent request budgets at four layers and self-tunes outbound limits automatically.