Config
Server
AI agents: fetch https://docs.erpc.cloud/config/server.llms.txt for the complete machine-readable version of this page (full configuration schema, defaults, worked examples, and source links). Append `.llms.txt` to any docs URL for the same treatment.AIFor agents: /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 Host header directly to a project and chain

Quick taste

Illustrative, not a tuned production config — minimal HTTP server with a 30-second ceiling:

server
erpc.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

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

FieldTypeDefaultBehavior / footguns
server.listenV4*booltrue (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*int4000IPv4 HTTP port. Startup fails with "server.httpPortV4 is not configured" if nil while listenV4 is true. common/defaults.go:L655-657
server.listenV6*boolunset (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*inthttpPortV4 + 1000 (default 5000)Derived to avoid collision with the default metrics port 4001. common/defaults.go:L658-668
server.httpPort*intdeprecated alias of httpPortV4If 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*boolfalseEnables gRPC. With default port derivation, gRPC shares the HTTP v4 port (in-handler mux + h2c). common/defaults.go:L669-671
server.grpcHostV4*stringcopy of httpHostV4common/defaults.go:L672-675
server.grpcPortV4*intcopy of httpPortV4Equal to HTTP by default — triggers port sharing. Set a different value for a standalone gRPC server. common/defaults.go:L676-679
server.grpcHostV6*stringcopy of httpHostV6No IPv6 port-sharing logic exists. common/defaults.go:L680-683
server.grpcPortV6*intcopy of httpPortV6common/defaults.go:L684-687
server.grpcMaxRecvMsgSize*int104857600 (100 MiB)gRPC max receive message size. common/defaults.go:L688-690
server.grpcMaxSendMsgSize*int104857600 (100 MiB)gRPC max send message size. common/defaults.go:L691-693
server.maxTimeout*Duration150sGlobal 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*Duration30shttp.Server.ReadTimeout — covers reading headers and body. common/defaults.go:L698-701
server.writeTimeout*Duration120shttp.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*booltrueWraps handler in gzipHandler for response compression. Inbound gzip is always accepted regardless of this flag. common/defaults.go:L706-708
server.tls.enabledboolfalseWhen 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.certFilestring""PEM cert path. Load failure → "failed to load TLS certificate and key".
server.tls.keyFilestring""PEM key path.
server.tls.caFilestring""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.insecureSkipVerifyboolfalseNo 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[]*AliasingRuleConfignil (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[].matchDomainstringWildcard/boolean pattern (*, |, &, !, parens) matched against the request host. common/matcher.go:L34-47
server.aliasing.rules[].serveProjectstring""Pre-selected project ID.
server.aliasing.rules[].serveArchitecturestring""Pre-selected architecture. Without serveProject, requires project in the URL path. Combining with serveChain but without serveProject is always ErrInvalidUrlPath.
server.aliasing.rules[].serveChainstring""Pre-selected chainId. Combining serveProject + serveChain without serveArchitecture is rejected at request time.
server.waitBeforeShutdown*Duration10sSleep 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*Duration10sSleep in Init after both listeners stop, before process exit. Lets telemetry exporters flush. common/defaults.go:L713-716
server.includeErrorDetails*booltrueWhen 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.responseHeadersmap[string]stringnilStatic 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*boolfalse (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.modeHealthCheckMode"networks""simple" = plain OK text; "networks" = per-network JSON; "verbose" = full JSON. erpc/healthcheck.go:L337-396
healthCheck.auth*AuthConfignilWhen set, a dedicated auth registry guards healthcheck endpoints. erpc/http_server.go:L201-207
healthCheck.defaultEvalstring"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[]stringnilJoined into Access-Control-Expose-Headers. erpc/http_server.go:L1057
cors.allowCredentials*boolfalseEmits Access-Control-Allow-Credentials: true only when true. erpc/http_server.go:L1059-1061
cors.maxAgeint3600Emits Access-Control-Max-Age when > 0. erpc/http_server.go:L1063-1065

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:

server
erpc.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}

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
erpc.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

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:

server
erpc.yaml
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):

server
erpc.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

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:

server.aliasing
erpc.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

Request/response behavior

HTTP status mapping (determineResponseStatusCode and handleErrorResponse, both switch on the same error code set):

StatusConditions
400ErrInvalidUrlPath, ErrJsonRpcRequestUnmarshal, ErrInvalidRequest
401ErrAuthUnauthorized, ErrEndpointUnauthorized
404ErrProjectNotFound, ErrNetworkNotFound, ErrNetworkNotSupported
429ErrAuthRateLimitRuleExceeded, ErrProjectRateLimitRuleExceeded, ErrNetworkRateLimitRuleExceeded, ErrEndpointCapacityExceeded
200everything 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>]}}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]
  • 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; each element is written as a streaming JSON array. They carry ONE aggregated X-ERPC-* diagnostic header set (counters summed, X-ERPC-Cache gains PARTIAL:<n>, slowest-sub-call duration, trace capped at 50 segments; no single-winner X-ERPC-Upstream) under the same executionHeaders mode — 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 unless executionHeaders: "off". erpc/http_server.go:L275-278
  • 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
  • X-ERPC-Cache-Attempts/-Retries/-Hedges — emitted only when the cache scope was exercised. erpc/http_server.go:L1170-1174
  • 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
  • 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 <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 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
  • traceparent / tracestate — W3C trace-context extraction on ingress. common/tracing_util.go:L67-73
  • ?eval= — healthcheck strategy override (bypasses healthCheck.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 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.
  • 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
  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
  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
  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
  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
  6. listenV6 has no default. IPv6 is OFF unless explicitly set to true; it is never auto-enabled. Source: common/defaults.go:L640-650
  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
  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
  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
  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
  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
  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
  13. serveProject + serveChain without serveArchitecture is always an error. Source: erpc/http_server.go:L985-990
  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
  15. X-ERPC-Attempts excludes NetworkAttempts to avoid double-counting rotations. Source: erpc/http_server.go:L1151-1157
  16. Batch detection is byte-exact. Leading whitespace before [ makes the body parse as a single (invalid) request. Source: 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
  18. RFC 7239 Forwarded is dead code — only headers named in trustedIPHeaders are consulted, parsed XFF-style. Source: erpc/http_server.go:L1856-1881
  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
  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
  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
  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
  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
  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
  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
  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
  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

Observability

MetricTypeLabelsWhen it fires
erpc_unexpected_panic_totalcounterscope, extra, errorScopes: request-handler, final-error-writer, top-level-handler, timeout-handler, validate-pattern
erpc_cors_requests_totalcounterproject (= URL path), originEvery request carrying an Origin header
erpc_cors_preflight_requests_totalcounterproject, originAllowed-origin OPTIONS preflight requests
erpc_cors_disallowed_origin_totalcounterproject, originOrigin 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)" 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

Related pages

  • Timeout — per-network timeout that must stay below server.maxTimeout or 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.