/config/server.llms.txt
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
Hostheader directly to a project and chain
Quick taste
Illustrative, not a tuned production config — minimal HTTP server with a 30-second ceiling:
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: 30sAgent 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
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
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
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
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
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 referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.
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
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
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
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
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
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
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
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
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.
| 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 |
server.httpHostV4 | *string | "0.0.0.0" | IPv4 bind host. common/defaults.go:L645-647 |
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 |
server.listenV6 | *bool | unset (OFF) | IPv6 must be opted in; SetDefaults never sets it. erpc/http_server.go:L191 |
server.httpHostV6 | *string | "[::]" | IPv6 bind host. common/defaults.go:L648-650 |
server.httpPortV6 | *int | httpPortV4 + 1000 (default 5000) | Derived to avoid collision with the default metrics port 4001. common/defaults.go:L658-668 |
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 |
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 |
server.grpcHostV4 | *string | copy of httpHostV4 | common/defaults.go:L672-675 |
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 |
server.grpcHostV6 | *string | copy of httpHostV6 | No IPv6 port-sharing logic exists. common/defaults.go:L680-683 |
server.grpcPortV6 | *int | copy of httpPortV6 | common/defaults.go:L684-687 |
server.grpcMaxRecvMsgSize | *int | 104857600 (100 MiB) | gRPC max receive message size. common/defaults.go:L688-690 |
server.grpcMaxSendMsgSize | *int | 104857600 (100 MiB) | gRPC max send message size. common/defaults.go:L691-693 |
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 |
server.readTimeout | *Duration | 30s | http.Server.ReadTimeout — covers reading headers and body. common/defaults.go:L698-701 |
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 |
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 |
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 |
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 |
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 |
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 |
server.aliasing.rules[].matchDomain | string | — | Wildcard/boolean pattern (*, |, &, !, parens) matched against the request host. common/matcher.go:L34-47 |
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 |
server.waitAfterShutdown | *Duration | 10s | Sleep in Init after both listeners stop, before process exit. Lets telemetry exporters flush. common/defaults.go:L713-716 |
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 |
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 |
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 |
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 |
server.executionHeaders | *ExecutionHeadersMode | "all" | "all" = counters + metadata + per-attempt X-ERPC-Upstreams log; "summary" = counters + metadata; "off" = no X-ERPC-* diagnostic headers. Batch responses get one aggregated set under the same mode. common/defaults.go:L720-723 |
server.costHeaders | *bool | false (common/defaults.go:L727-729) | Opt-in cost/billing headers on single and batch responses: X-ERPC-Calls, X-ERPC-Billable, X-ERPC-Methods, X-ERPC-Credits, X-ERPC-Credits-Total, X-ERPC-Credits-Version. Pricing itself is vendor-owned (CreditUnitsProvider.CreditUnits(req, upstreamCfg) — nothing hard-coded in the eRPC layer): vendors ship their public tables, overridable per method via providers[].settings.creditUnits (or upstreams[*].creditUnits); vendors without pricing cost a flat 1 credit per request. erpc/http_server.go:L1326-1384 |
healthCheck.mode | HealthCheckMode | "networks" | "simple" = plain OK text; "networks" = per-network JSON; "verbose" = full JSON. erpc/healthcheck.go:L337-396 |
healthCheck.auth | *AuthConfig | nil | When set, a dedicated auth registry guards healthcheck endpoints. erpc/http_server.go:L201-207 |
healthCheck.defaultEval | string | "any:initializedUpstreams" | Default eval strategy when ?eval= query param is absent. erpc/healthcheck.go:L106-112 |
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 |
cors.allowedMethods | []string | ["GET","POST","OPTIONS"] | Joined into Access-Control-Allow-Methods. 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 |
cors.exposedHeaders | []string | nil | Joined into Access-Control-Expose-Headers. erpc/http_server.go:L1057 |
cors.allowCredentials | *bool | false | Emits Access-Control-Allow-Credentials: true only when true. erpc/http_server.go:L1059-1061 |
cors.maxAge | int | 3600 | Emits Access-Control-Max-Age when > 0. erpc/http_server.go:L1063-1065 |
Hardcoded constants (non-configurable):
IdleTimeout= 300s on both serversMaxHeaderBytes= 1 MiB — the only hard inbound size limit- No request-body size limit —
util.ReadAllcopies until EOF with no upper bound; effective limits arereadTimeoutand 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:
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:
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: 30s3. 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:
server: httpPortV4: 4000 grpcEnabled: true # different port → standalone gRPC server, HTTP keeps its TimeoutHandler grpcPortV4: 40104. 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):
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: 5s5. 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:
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: mainRequest/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
Error response shapes:
- Standard JSON-RPC single error:
{"jsonrpc":"<ver>","id":<id>,"error":{"code":<int>,"message":"<msg>"[,"data":<any>]}}—includeErrorDetailsaddserror.dataexcept oneth_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] - 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":...}}— noidfield - Batch responses are always HTTP 200; each element is written as a streaming JSON array. They carry ONE aggregated
X-ERPC-*diagnostic header set (counters summed,X-ERPC-CachegainsPARTIAL:<n>, slowest-sub-call duration, trace capped at 50 segments; no single-winnerX-ERPC-Upstream) under the sameexecutionHeadersmode — see directives. Source:erpc/http_server.go:L706-712
Diagnostic response headers (per-response set on singles via setResponseHeaders; aggregated set on batches via writeBatchExecHeaders):
X-ERPC-Version,X-ERPC-Commit— build-identity headers (ldflags vars, defaults"dev"and"none"); emitted unlessexecutionHeaders: "off".erpc/http_server.go:L275-278X-ERPC-Attempts,X-ERPC-Upstream-Attempts/-Retries/-Hedges,X-ERPC-Network-Attempts/-Retries/-Hedges— counters, zero-filled on early errors (not omitted; onlymode: offremoves them).erpc/http_server.go:L1149-1166X-ERPC-Cache-Attempts/-Retries/-Hedges— emitted only when the cache scope was exercised.erpc/http_server.go:L1170-1174X-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-1183X-ERPC-Cache: HIT|MISS,X-ERPC-Upstream,X-ERPC-Duration— response metadataX-ERPC-Upstreams(modeallonly): per-attempt participation log in the form<id>=<reason>:<outcome>:<duration>ms[:won]traceparent(+tracestate) — injected when tracing is enabled.common/tracing_util.go:L58-65
Request headers with server-level effects:
Host— aliasing match (port stripped)Content-Encoding: gzip— inbound body decompression (always, regardless ofenableGzip)Accept-Encoding: gzip— response compression eligibilityContent-Type: application/grpcover HTTP/2 — gRPC mux on shared port (bypassesTimeoutHandlerand gzip)Origin— CORS evaluation; absent Origin bypasses CORS entirelyX-ERPC-Force-Trace: true|1|yesor?force-trace=true|1|yes— force-sample the OTel trace for this request.common/tracing_util.go:L106-119traceparent/tracestate— W3C trace-context extraction on ingress.common/tracing_util.go:L67-73?eval=— healthcheck strategy override (bypasseshealthCheck.defaultEval).erpc/healthcheck.go:L106-112- 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
Best practices
- Set
maxTimeoutas a Go duration string ("30s"), never a bare integer.maxTimeout: 150means 150 milliseconds, not 150 seconds — this is the most common misconfiguration footgun. - Size
maxTimeoutto be greater than your largest network-levelfailsafe.timeoutplus total retry overhead, or retries will be cut short by the server ceiling before they complete. See Timeout. - In production behind an LB, set
waitBeforeShutdownto at least two readiness-probe intervals (often 20s). The default10sis too short for many Kubernetes setups. - Never omit
trustedIPForwarderswhen 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.caFileenables 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 verboseX-ERPC-Upstreamsheader that grows linearly with the number of upstream attempts. - Use
responseHeadersfor 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
maxTimeoutbare integer is milliseconds.maxTimeout: 150= 150 ms, not 150 s. Always use a duration string like"150s". Source:common/duration.go:L20-31grpcEnabled: truewith default ports silently shares the HTTP v4 port. gRPC traffic bypassesmaxTimeoutand gzip. Source:erpc/http_server.go:L161-177- mTLS is implicit. Setting
tls.caFileflipsClientAuth = RequireAndVerifyClientCert— all clients must present certs. No other field is needed. Source:erpc/http_server.go:L1621-1633 tls.insecureSkipVerifydoes nothing for inbound connections. It is a client-dialing flag only; Go's TLS server ignores it. Inbound cert verification is controlled bycaFilealone. Source:erpc/http_server.go:L1635responseHeadersenv 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-148listenV6has no default. IPv6 is OFF unless explicitly set totrue; it is never auto-enabled. Source:common/defaults.go:L640-650httpPort(deprecated) is silently migrated. No warning is emitted. AfterSetDefaults,httpPortalways equalshttpPortV4. New configs must usehttpPortV4. Source:common/defaults.go:L652-661- No request-body size limit. There is no
http.MaxBytesReader; a multi-GB body is fully read into memory. OnlyreadTimeoutand process memory provide back-pressure. Source:util/reader.go:L12-32 - 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 - Whole responses are buffered before hitting the socket. Large responses consume RAM; pooled buffers above 256 KiB are discarded to GC.
writeTimeoutonly matters at the final socket copy. Source:erpc/http_timeout.go:L43-48 - 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 - Zero-project configs self-alias. A default
mainproject andmatchDomain: "*"rule are injected so/evm/1works without a project segment. Source:common/defaults.go:L100-111 serveProject+serveChainwithoutserveArchitectureis always an error. Source:erpc/http_server.go:L985-990- 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 X-ERPC-Attemptsexcludes NetworkAttempts to avoid double-counting rotations. Source:erpc/http_server.go:L1151-1157- Batch detection is byte-exact. Leading whitespace before
[makes the body parse as a single (invalid) request. Source:erpc/http_server.go:L405 - Any non-POST/non-OPTIONS request becomes a healthcheck, including
GET /myproject/evm/1. Source:erpc/http_server.go:L1001-1003 - RFC 7239
Forwardedis dead code — only headers named intrustedIPHeadersare consulted, parsed XFF-style. Source:erpc/http_server.go:L1856-1881 - CORS metric label mismatch. The label named
projectis populated with the URL path, not the project ID. Source:erpc/http_server.go:L1020 serveArchitecture-only aliasing requires a project in the URL path. CombiningserveArchitecture+serveChainwithoutserveProjectis alwaysErrInvalidUrlPath. Source:erpc/http_server.go:L963-990- OPTIONS to a project without CORS config falls through to normal request handling. The OPTIONS early-return lives inside the
CORS != nilbranch; a project with nocors: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 - 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 - 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 - 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., becausewriteCounterHeadersruns against a nil-safe snapshot. OnlyexecutionHeaders: "off"suppresses them entirely. Source:erpc/http_server.go:L1149-1166 ErrUnknownfallback body is not JSON-RPC shaped. WhenprocessErrorBodyreceives an error that survives all unwrapping as neither a*common.BaseErrornorcommon.StandardError, it produces{"code":"ErrUnknown","message":"unexpected server error","cause":{...}}— a struct dump, not{"jsonrpc":"2.0","error":{...}}. Clients parsingresponse.error.codeas an integer will fail; detection must branch on whether the outer object has acodestring key vs anerrorobject key. Source:erpc/http_server.go:L1450-1454ErrorStatusCode()on error types is dead code. Every error type incommon/errors.goimplementsErrorStatusCode() int, but there are no call sites. The wire HTTP status is determined exclusively by the two switch blocks indetermineResponseStatusCodeandhandleErrorResponseusingcommon.HasErrorCode. Reading an error type'sErrorStatusCode()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- 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
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 <addr>"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)"forresponseHeaders"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(opens in a new tab) —NewHttpServer: handler chain assembly, gzip/timeout wrapping, gRPC mux, IPv4/IPv6 server construction,responseHeadersenv expansion, trusted forwarder parsingcommon/defaults.go:L640-L733(opens in a new tab) —ServerConfig.SetDefaults: all defaults including port derivation, deprecatedhttpPortmigration, zero-project aliasing injectionerpc/http_timeout.go:L20-L143(opens in a new tab) — customTimeoutHandler: buffered response, timeout/cancel body shapes, panic propagationerpc/http_server.go:L1537-L1637(opens in a new tab) — TLS config construction,ListenAndServeTLS, mTLSClientAuthassignmenterpc/http_server.go:L1782-L1905(opens in a new tab) —resolveRealClientIP: trusted forwarder check, XFF right-trim, fallback to peer IPerpc/http_server.go:L810-L1006(opens in a new tab) —parseUrlPath: full aliasing × path-segment case matrixcommon/validation.go:L70-L109(opens in a new tab) —ServerConfig.Validate: listen host/port pairing,maxTimeoutrequired, forwarder IP/CIDR syntaxerpc/http_server_test.go(opens in a new tab) — path parsing matrix, CORS, timeouts, aliasing fixtures
Related pages
- Timeout — per-network timeout that must stay below
server.maxTimeoutor the server ceiling fires first. - Rate limiters — per-IP rate limits depend on accurate client IP, which requires correct
trustedIPForwarders+trustedIPHeaders. - Auth — authentication strategies consume the resolved client IP from this layer.
- URL structure & aliasing — full path-parsing logic and all valid segment combinations for domain aliasing.
- Deployment overview — where
server:fits in a production Kubernetes or Docker deployment.