Explain how RequestPredicate matching works internally (state changes on ServerRequest, nested routing) and how you'd write a custom RequestPredicate.
answer
- test(ServerRequest) + nest(ServerRequest) + accept(Visitor)
- matching mutates request: pathVariables + MATCHING_PATTERN_ATTRIBUTE
- nest consumes the path prefix
- and() chains nest so state accumulates
- keep test() pure/non-blocking on the event loop
basics
~20 sA RequestPredicate implements test(ServerRequest). Matching can have side effects — it stores path variables and the matched pattern on the request, and nested()/subRoute() can mutate the request (e.g. consume a path prefix). Implement RequestPredicate and override test() (and nest()) for custom logic.
solid answer
~40 s`RequestPredicate` has `boolean test(ServerRequest)` plus optional `Optional<ServerRequest> nest(ServerRequest)` used by nested routing. Matching isn't purely functional: when `path(...)` matches it stores captured path variables and the `MATCHING_PATTERN_ATTRIBUTE` on the `ServerRequest` attributes map, and in `nest(...)` it returns a request whose path has the matched prefix removed so inner routes see the remainder. Composition via `and`/`or`/`negate` wraps these behaviors — `and` also composes the `nest` step so state accumulates. To write your own, implement `RequestPredicate` and override `test()` (returning a boolean off `ServerRequest`), optionally `accept(Visitor)` for debug output. Common real cases: a header-value predicate, a feature-flag check, or an API-version predicate. You typically prefer composing existing predicates or `RequestPredicates.headers(Predicate<Headers>)`/`queryParam(name, Predicate)` before hand-rolling one.
code
java · 20 linesimport org.springframework.web.reactive.function.server.*;
class ApiVersionPredicate implements RequestPredicate {
private final String version;
ApiVersionPredicate(String version) { this.version = version; }
@Override public boolean test(ServerRequest request) {
return version.equals(request.headers().firstHeader("X-Api-Version"));
}
@Override public void accept(RequestPredicates.Visitor visitor) {
visitor.header("X-Api-Version", version); // nicer route introspection
}
}
// compose like any built-in predicate
RouterFunction<ServerResponse> routes(Handler h) {
return RouterFunctions.route(
RequestPredicates.GET("/users").and(new ApiVersionPredicate("2")),
h::listV2);
}go deeper
Know you can implement RequestPredicate.test() for custom conditions.
Explain that matching stores path variables on the request and how to compose a custom predicate with and().
Describe nest() prefix consumption and the non-blocking constraint on test().
Cover the full contract (test/nest/accept), state-mutation semantics, event-loop safety, and when a routing predicate is the right abstraction vs handler logic.
**The interface.** `RequestPredicate` (in `org.springframework.web.reactive.function.server`) declares: - `boolean test(ServerRequest request)` — the core match. - `default Optional<ServerRequest> nest(ServerRequest request)` — used by `RouterFunctions.nest(...)`; returns a (possibly transformed) request if the predicate matches for nesting purposes, else empty. - `default void accept(RequestPredicates.Visitor visitor)` — lets tooling render the predicate (used by `RouterFunction.toString()` / route introspection). - default `and`, `or`, `negate` combinators. **Matching has side effects.** This surprises people: predicates are not side-effect-free. - When a `path(...)` predicate matches, it **puts captured path variables** into the request's attribute map (`RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE`, exposed via `pathVariable()`), and records the matched pattern under `RouterFunctions.MATCHING_PATTERN_ATTRIBUTE`. - In **nested routing**, `path(...).nest(request)` returns a request whose remaining path has the matched prefix **removed**, so inner routes match against the leftover. This is prefix consumption. **Composition semantics.** `and(other)` builds a predicate whose `test` is `this.test && other.test`, and whose `nest` chains: it first nests `this`, and if present nests `other` on the resulting request, so accumulated state (consumed prefix, captured vars) flows through. `or(other)` tries `this`, falling back to `other`. `negate()` inverts `test`. Short-circuit evaluation applies. **Routing pipeline.** `RouterFunctionMapping` obtains the composed `RouterFunction` and calls `route(ServerRequest)`, which walks routes in declaration order, calling each predicate's `test`; the first true returns that route's `HandlerFunction` wrapped as the mapping result, then a `HandlerFunctionAdapter` invokes it. `nest(...)` is handled by a `NestedRouterFunction` that applies the predicate's `nest` to transform the request before evaluating child routes. **Writing a custom predicate.** Prefer built-ins/`headers(...)`/`queryParam(name, Predicate)` first. When you truly need custom logic: ```java class ApiVersion implements RequestPredicate { private final String v; ApiVersion(String v){ this.v = v; } public boolean test(ServerRequest r){ return v.equals(r.headers().firstHeader("X-Api-Version")); } public void accept(RequestPredicates.Visitor visitor){ visitor.header("X-Api-Version", v); // for nice toString/introspection } } ``` Use it like any other: `route(GET("/x").and(new ApiVersion("2")), handler)`. Keep `test` cheap and **pure of blocking I/O** — predicates run on the event loop, so never block (no synchronous DB/HTTP calls); do such checks inside the reactive handler instead. **Edge cases / gotchas.** - Don't perform blocking or reactive-subscribing work in `test` — it must be synchronous and fast. - If you override a path-consuming behavior, also implement `nest` correctly or nesting breaks. - Because matching mutates request attributes, testing predicates in isolation should use a fresh `ServerRequest` (e.g., `MockServerRequest`) each time. - Implement `accept(Visitor)` if you want your predicate to show up meaningfully in route listings/actuator mappings. **When to use.** Reach for a custom `RequestPredicate` for cross-cutting routing conditions (API versioning, tenant/feature flags) that must influence *which handler* runs. If the condition only affects behavior *within* a handler, do it in the handler, not a predicate.
- Why must a custom RequestPredicate's test() never block?Predicates run synchronously on the Reactor event-loop thread during route selection; a blocking call (DB/HTTP) would stall the loop. Any blocking/reactive work belongs inside the HandlerFunction, not the predicate.
- What does the nest() method do and why does and() compose it?nest() lets a predicate transform the request for nested routing — e.g. path() removes the matched prefix so inner routes see the remainder. and() chains nest so consumed prefix and captured path variables accumulate through composed predicates.
- How would you make your custom predicate visible in route listings?Override accept(RequestPredicates.Visitor) and describe the predicate (e.g. visitor.header(...)); the Visitor is how RouterFunction toString/introspection renders predicates.
saying these in an interview costs you the question
- Believing predicates are pure/side-effect-free (they set path variables and matched-pattern attributes)
- Doing blocking I/O inside test()
- Ignoring nest() so nested routing loses prefix consumption or path variables
- Subscribing to a Mono inside a predicate