You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After the header-mutation work landed in #1231, the closing notes of #596 flagged header-based routing as the remaining gap: the router
still keys on (hostname, path, method) only, and a fresh narrowly-scoped
issue was suggested as the cleaner path forward (rather than folding
into #1239's general programmable hooks).
This is that issue. It proposes a declarative header-predicate
surface on RequestHttpFrontend, distinct from #1239's hooks: a static
config field that any provider (CLI, config file, command socket
consumer) can fill without writing code.
Use case (Sozune): Sozune is a Traefik-style higher-level proxy on
top of sozu-lib (referenced in #1239). Today it can express Host(...), PathPrefix(...), Method(...) — equivalents to Traefik's
matchers — but Headers(name, value) and HeaderRegexp(name, regex)
are blocked at the RequestHttpFrontend boundary. Concrete operator
labels would look like:
This is a recurrent ask on the Traefik-comparison axis (canary by header,
B2B per-tenant routing, gRPC service split via :authority + custom
headers, A/B by X-Variant, etc.).
#1239 (programmable Rust hooks) would technically cover this — write a
hook that inspects headers and returns a routing decision. But for
header-equality / regex / presence checks specifically:
Every operator using header-based routing would re-implement the same
predicate evaluator, with diverging semantics.
The configuration round-trip (sozuctl, command socket, config file)
loses the predicate — it's hidden inside compiled-in Rust code.
state.rs reconciliation, frontend listing, and operational tooling
can't see why a frontend was chosen.
A declarative predicate keeps parity with how path, method, and hostname are already expressed: serializable, reloadable, listable. #1239 remains the right home for arbitrary logic (token validation,
dynamic backend selection, custom rewrite rules); header equality /
regex / presence is the long-tail that deserves first-class support.
Proposed proto change
Add one field to RequestHttpFrontend in command/src/command.proto:
message RequestHttpFrontend{// ... existing fields 1..16 unchanged ...// Additional header predicates that must all hold for this// frontend to match. Empty = no header constraint (current// behaviour). Names are matched case-insensitively per RFC 9110// §5.1, consistent with the existing `Header` mutation type.
repeated HeaderMatch match_headers = 17;}
message HeaderMatch{// Header name, case-insensitive (RFC 9110 §5.1).
required string name = 1;// Exactly one predicate must be set.
oneof predicate {// Exact value match (byte-for-byte after trimming OWS).
string exact = 2;// RE2-compatible regex applied to the full header value.
string regex = 3;// Header must be present; value is not inspected.
bool present = 4;}}
Semantics
Combination: all entries in match_headers must hold (logical
AND), matching Traefik's Headers(...) semantics. OR is expressed
by registering multiple frontends with the same (host, path, method).
Multi-valued headers: a request header repeated N times (or
comma-folded) matches if any of its values satisfies the
predicate. This avoids surprises with Accept, Cookie, Forwarded.
Precedence vs. unconstrained frontends: when several frontends
match (hostname, path, method), the one with a non-empty match_headers is preferred over one without. Among frontends with
non-empty match_headers, current RulePosition ordering applies,
then the more specific (longer match_headers) wins. Without this
rule, a header-less fallback frontend always shadows a
header-specific one — defeats the feature.
Empty list: preserves current behaviour exactly. No migration
for existing deployments.
Router implementation sketch
The (hostname, path, method) lookup in lib/src/router/mod.rs already
returns a Vec<HttpFront> candidate set (per #571 closing notes — domain
trie returns a vec, path/method linearly tested). Header predicates
slot into that linear post-filter:
Trie lookup → candidate vec (unchanged).
Existing path + method filter (unchanged).
New: for each remaining candidate with a non-empty match_headers, evaluate predicates against HttpContext headers.
Drop the candidate if any predicate fails.
Sort surviving candidates by (header-specific first, then RulePosition, then predicate count) and pick the first.
Headers are already fully parsed before Router::connect runs (post #1231, kawa H1 + Mux session layer), so no buffering changes are needed.
RE2 is already a transitive dep via regex in the kawa parsing path.
Out of scope (deliberately)
Header value transformations / capture groups in routing (handled by
the existing headers mutation field).
Body-based routing.
Routing on connection metadata (client IP, TLS SNI beyond hostname) —
those deserve their own predicates if/when needed.
Query-string predicates — same shape, separate ticket to keep this
one focused.
Open questions for maintainers
Field tag: is 17 acceptable on RequestHttpFrontend, or do you
want a reserved range for future predicates?
Regex flavour: confirm RE2 (via regex crate) is the right
choice rather than PCRE-flavoured syntax.
Trimming: match against trimmed value (OWS removed per RFC 9110
§5.5) or raw bytes? Traefik trims; HAProxy doesn't.
Forbidden header names: should we reject predicates on Host (already routed on) and Cookie (sticky-session collision
risk) at config-load, or trust operators?
Implementation appetite: would a PR from the Sozune side be
welcome, or is this on the Sōzu roadmap and a contribution would
conflict?
Hello,
After the header-mutation work landed in #1231, the closing notes of
#596 flagged header-based routing as the remaining gap: the router
still keys on
(hostname, path, method)only, and a fresh narrowly-scopedissue was suggested as the cleaner path forward (rather than folding
into #1239's general programmable hooks).
This is that issue. It proposes a declarative header-predicate
surface on
RequestHttpFrontend, distinct from #1239's hooks: a staticconfig field that any provider (CLI, config file, command socket
consumer) can fill without writing code.
Use case (Sozune): Sozune is a Traefik-style higher-level proxy on
top of
sozu-lib(referenced in #1239). Today it can expressHost(...),PathPrefix(...),Method(...)— equivalents to Traefik'smatchers — but
Headers(name, value)andHeaderRegexp(name, regex)are blocked at the
RequestHttpFrontendboundary. Concrete operatorlabels would look like:
This is a recurrent ask on the Traefik-comparison axis (canary by header,
B2B per-tenant routing, gRPC service split via
:authority+ customheaders, A/B by
X-Variant, etc.).Why declarative, not via #1239
#1239 (programmable Rust hooks) would technically cover this — write a
hook that inspects headers and returns a routing decision. But for
header-equality / regex / presence checks specifically:
predicate evaluator, with diverging semantics.
sozuctl, command socket, config file)loses the predicate — it's hidden inside compiled-in Rust code.
state.rsreconciliation, frontend listing, and operational toolingcan't see why a frontend was chosen.
A declarative predicate keeps parity with how
path,method, andhostnameare already expressed: serializable, reloadable, listable.#1239 remains the right home for arbitrary logic (token validation,
dynamic backend selection, custom rewrite rules); header equality /
regex / presence is the long-tail that deserves first-class support.
Proposed proto change
Add one field to
RequestHttpFrontendincommand/src/command.proto:Semantics
Combination: all entries in
match_headersmust hold (logicalAND), matching Traefik's
Headers(...)semantics. OR is expressedby registering multiple frontends with the same
(host, path, method).Multi-valued headers: a request header repeated N times (or
comma-folded) matches if any of its values satisfies the
predicate. This avoids surprises with
Accept,Cookie,Forwarded.Precedence vs. unconstrained frontends: when several frontends
match
(hostname, path, method), the one with a non-emptymatch_headersis preferred over one without. Among frontends withnon-empty
match_headers, currentRulePositionordering applies,then the more specific (longer
match_headers) wins. Without thisrule, a header-less fallback frontend always shadows a
header-specific one — defeats the feature.
Empty list: preserves current behaviour exactly. No migration
for existing deployments.
Router implementation sketch
The
(hostname, path, method)lookup inlib/src/router/mod.rsalreadyreturns a
Vec<HttpFront>candidate set (per #571 closing notes — domaintrie returns a vec, path/method linearly tested). Header predicates
slot into that linear post-filter:
match_headers, evaluate predicates againstHttpContextheaders.Drop the candidate if any predicate fails.
RulePosition, then predicate count) and pick the first.Headers are already fully parsed before
Router::connectruns (post#1231, kawa H1 + Mux session layer), so no buffering changes are needed.
RE2 is already a transitive dep via
regexin thekawaparsing path.Out of scope (deliberately)
the existing
headersmutation field).those deserve their own predicates if/when needed.
one focused.
Open questions for maintainers
RequestHttpFrontend, or do youwant a reserved range for future predicates?
regexcrate) is the rightchoice rather than PCRE-flavoured syntax.
§5.5) or raw bytes? Traefik trims; HAProxy doesn't.
Host(already routed on) andCookie(sticky-session collisionrisk) at config-load, or trust operators?
welcome, or is this on the Sōzu roadmap and a contribution would
conflict?
Refs
Headers(),HeadersRegexp()matchersheadermatcherRouteMatch.headers