A gateway sits in front of every request, so the knobs you can't reach are the ones you end up fighting at 2am. Solon Cloud Gateway 4.0.5 (tagged 2026-08-10) opens up almost every one of them: predicates and filters can be disabled per-factory, X-Forwarded-* outbound header generation is fully controllable, the HttpClient gets pool/websocket/proxy/SSL sections, and — the one I'd call a quiet security fix — the RemoteAddr predicate no longer trusts X-Forwarded-For. Here's what changed and how to use it.
The nine changes at a glance
| What | Config prefix | Kind |
|---|---|---|
| Predicate factory on/off | solon.cloud.gateway.predicate.* |
bool |
| Filter factory on/off | solon.cloud.gateway.filter.* |
bool |
| X-Forwarded-* header generation | solon.cloud.gateway.xForwarded.* |
map |
| HttpClient connection pool | solon.cloud.gateway.httpClient.pool.* |
map |
| HttpClient WebSocket behavior | solon.cloud.gateway.httpClient.websocket.* |
map |
| HttpClient outbound proxy | solon.cloud.gateway.httpClient.proxy.* |
map |
| HttpClient mTLS / trust store | solon.cloud.gateway.httpClient.ssl.* |
map |
New XForwardedRemoteAddr predicate |
n/a (route config) | — |
RemoteAddr predicate semantics |
n/a (route config) | — |
All of them live in org.noear.solon.cloud.gateway and bind through GatewayProperties under the solon.cloud.gateway root.
Toggle predicates and filters like feature flags
Before 4.0.5 every registered factory was always active. Now GatewayProperties carries two boolean maps — predicate and filter — bound to solon.cloud.gateway.predicate.* and solon.cloud.gateway.filter.*:
solon:
cloud:
gateway:
predicate:
remote-addr: false # disable the RemoteAddr predicate
filter:
strip-prefix: false # disable the StripPrefix filter
The map key is the factory prefix, but you don't have to match casing: RouteFactoryManager normalizes keys (lowercase, hyphens/underscores stripped), so remote-addr, remote_addr, and RemoteAddr all resolve to the same factory. A disabled factory is treated as unregistered — getPredicate() / getFilter() return null, the route simply skips that step, and nothing throws.
This matters more than it sounds. If you don't use cookies or query predicates in front of a public endpoint, switching them off is a small but real reduction of the attack surface and removes dead code paths from the routing hot path. The gateway registers ten predicate factories and eight filter factories by default:
Predicates: After, Before, Cookie, Header, Host, Method, Path, Query, RemoteAddr, XForwardedRemoteAddr
Filters: AddRequestHeader, AddResponseHeader, PrefixPath, RedirectTo, RewritePath,
RemoveRequestHeader, RemoveResponseHeader, StripPrefix
Control the X-Forwarded-* headers you emit
XForwardedProperties (new in 4.0.5) governs which X-Forwarded-* headers the gateway writes on outbound requests. It has a master switch plus per-header enabled/append pairs:
solon:
cloud:
gateway:
xForwarded:
enabled: true # master switch; false = emit no X-Forwarded-* at all
forEnabled: true # X-Forwarded-For
forAppend: true # true = append the peer socket IP hop by hop; false = overwrite
hostEnabled: true # X-Forwarded-Host (from the original Host header)
hostAppend: false # overwrite mode
portEnabled: true # X-Forwarded-Port (defaults to 80/443 for http/https)
portAppend: false
protoEnabled: true # X-Forwarded-Proto (http/https)
protoAppend: false
The defaults follow one simple rule: for appends, host/port/proto overwrite. X-Forwarded-For accumulates each hop's socket IP so downstream services can see the real chain; the other three are treated as single-value facts about the original request. If you run multiple layers of proxies, you'll likely want forAppend: false at the outermost edge so the client-provided value is replaced rather than extended.
HttpClient: every dial turned into configuration
The HttpClientProperties (extends TimeoutProperties) now exposes five areas — and 4.0.5 also tightens the default response timeout from 1800s down to 60s (0 disables it):
solon:
cloud:
gateway:
httpClient:
compression: true # outbound GZip compression (default false)
connectTimeout: 10 # seconds
requestTimeout: 10 # seconds, waiting for upstream response headers
responseTimeout: 60 # seconds, overall fallback; 0 = disabled
pool:
maxConnections: 250 # per upstream host, ≈ QPS × avg response seconds
maxWaitQueueSize: 1000 # queue limit; overflow → fast-fail 503; -1 = unlimited
maxIdleTime: 60 # idle seconds, matches common LB 60s idle drop
keepAliveTimeout: 60
maxPools: 256 # pool count; excess → LRU eviction to prevent leaks
websocket:
maxConnections: 200
idleTimeout: 60
closingTimeout: 10
pingInterval: 30 # heartbeat seconds; ≤ 0 disables
proxy:
enabled: false
host: 127.0.0.1
port: 8080
type: HTTP # HTTP | SOCKS4 | SOCKS5
username: ""
password: ""
nonProxyHostsPattern: "" # regex of hosts that bypass the proxy
ssl:
enabled: false
keyStore: "" # client cert keystore path
keyStorePassword: ""
keyStoreType: JKS # JKS | PKCS12
keyPassword: "" # falls back to keyStorePassword
trustStore: "" # custom trust store
trustStorePassword: ""
trustStoreType: JKS
A few defaults are worth internalizing: the pool is per-upstream-host and sized by maxConnections: 250; when the pool is full and the wait queue exceeds maxWaitQueueSize: 1000, requests fast-fail with 503 instead of hanging forever; maxPools: 256 bounds the number of host pools and LRU-evicts the idle ones so a drifting set of upstream instances can't leak pools. The SSL section turns the gateway's outbound HTTP client into an mTLS client — useful when the upstream side expects client certificates.
The one that matters: RemoteAddr stopped trusting X-Forwarded-For
This is the change I'd flag in any upgrade review. Up to v4.0.0, RemoteAddrPredicateFactory.test() matched against ctx.realIp():
// v4.0.0 and earlier — trusts client-supplied headers
public boolean test(ExContext ctx) {
String ip = ctx.realIp();
...
}
And ExContextImpl.realIp() resolves in this order: X-Real-IP header → X-Forwarded-For header (first value before the comma) → socket peer address. All three of the header sources are client-controlled. Anyone who can set headers can spoof them:
GET /admin HTTP/1.1
X-Forwarded-For: 10.0.0.1
If a gateway in front of an internal admin panel used RemoteAddr=10.0.0.0/8 as an allowlist, that request sailed through — because the check trusted the header, not the connection.
In 4.0.5 the semantics are split cleanly:
-
RemoteAddrmatches the pure TCP peer —ctx.remoteAddress().host()— and trusts no client header. If there is no peer address it returnsfalse. This is the one to use for IP allowlists on internet-facing gateways. -
XForwardedRemoteAddrPredicateFactorymatchesctx.realIp()and is the explicit opt-in when you have a trusted proxy chain in front (Nginx, cloud LB) that rewrites the headers for you.
# route config — CIDR notation, same syntax for both
# RemoteAddr=192.168.1.1/24 → peer socket IP only
# XForwardedRemoteAddr=192.168.1.1/24 → trusts X-Forwarded-For (opt-in)
The new factory even guards against a subtle failure: if the header value is a hostname rather than an IP literal, it fails the match instead of triggering a synchronous DNS lookup on the event loop.
A small ecosystem echo
The gateway's websocket.pingInterval: 30 (heartbeat every 30s, ≤0 disables) pairs nicely with the main framework's 4.0.5 addition of sendPing() / sendPong() on the WebSocket API — you can now configure the gateway to heartbeat upstream WebSocket connections, and application handlers can answer pings explicitly. Two repos, one heartbeat story.
Upgrade checklist
-
Audit every
RemoteAddrroute — if it relied on spoofableX-Forwarded-For, switch toXForwardedRemoteAddr(trusted proxy chain) or move the allowlist check to the trusted edge. - Tighten timeouts — the default response timeout already dropped 1800s → 60s; set explicit values per route rather than inheriting.
-
Right-size the pool — start from
maxConnections ≈ QPS × avg response secondsand keepmaxWaitQueueSizebounded so overload returns 503 fast instead of queuing forever. -
Trim the factory registry —
predicate.*/filter.*off-switches for factories you never use. - Decide your X-Forwarded-For policy — append at internal hops, overwrite at the trusted edge.
Solon Cloud Gateway 4.0.5 doesn't add a new protocol or a new panel — it gives you back control over the machinery that was already running your traffic, and it quietly closes the spoofed-IP gap that the old RemoteAddr behavior left open. That second part alone is worth the upgrade.
(Part of the Solon v4.0.5 release series — see also the solon-ai 4.0.5 HTTP-customization article.)
Top comments (0)