This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/docs by this push:
     new 6fe7ecce24 docs: TODO-72 mixins/paths topic page + 9.5 release notes + 
sidebar (incl. 10.20b conditional-GET + 15.10 health probes)
6fe7ecce24 is described below

commit 6fe7ecce24506f7e79afc1272662c201d601f19f
Author: James Bognar <[email protected]>
AuthorDate: Sat May 23 09:27:04 2026 -0400

    docs: TODO-72 mixins/paths topic page + 9.5 release notes + sidebar (incl. 
10.20b conditional-GET + 15.10 health probes)
    
    Co-authored-by: Cursor <[email protected]>
---
 pages/release-notes/9.5.0.md                    |  98 ++++++
 pages/topics/10.07a.RestServerComposition.md    | 389 ++++++++++++++++++++++++
 pages/topics/10.20b.RestServerConditionalGet.md | 232 ++++++++++++++
 pages/topics/15.10.HealthProbes.md              | 156 ++++++++++
 pages/topics/23.01.V9.5-migration-guide.md      |   6 +
 sidebars.ts                                     |  15 +
 6 files changed, 896 insertions(+)

diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index bf119275b1..36a707736a 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -1986,6 +1986,39 @@ String name
 
 ### juneau-rest-server
 
+#### Health Probe SPI + Resource (TODO-65)
+
+`juneau-rest-server` now includes a built-in probe SPI and aggregation 
resource under
+`org.apache.juneau.rest.health`:
+
+- `HealthIndicator` functional SPI (`Health check()`).
+- `Health` value type with static builders: `Health.up(name)`, 
`Health.down(name, throwable)`,
+  `Health.unknown(name)`, plus structured detail entries.
+- `HealthProbe` / `HealthStatus` enums for probe categorization and aggregate 
state.
+- `BasicHealthResource` exposing `/healthz`, `/readyz`, and `/livez` with 
aggregate body format:
+  `{status, components:{name:{status,details}}}`.
+- HTTP status mapping follows Kubernetes conventions: `503` if any component 
is `DOWN`, else `200`.
+- `HealthProbeSettings` introduces per-indicator timeout control (default 
`1s`); exceptions/timeouts
+  are converted to `DOWN` with `details.error`.
+
+#### `@Rest(mixins=...)` Composition + `@Rest(paths=...)` Multi-Mount
+
+See [REST Server — Mixins and Multi-Mount 
Paths](/docs/topics/RestServerCompositionMixinsAndPaths) for the full reference.
+
+The health-probe follow-up adds two new `@Rest` composition/mounting 
primitives:
+
+- **`mixins`** (`Class<?>[]`) — graft `@RestOp`-group methods from mixin 
classes into a resource's
+  operation tree. Local methods on the importing resource win on path 
collisions.
+- **`paths`** (`String[]`) — optional multi-mount path specs for top-level 
servlet registration.
+  Jetty mounts a single servlet instance at each declared path.
+
+`BasicHealthResource` now declares 
`@Rest(paths={"/healthz","/readyz","/livez"})` instead of
+`@Rest(path="/")`, eliminating startup collisions with apps whose root 
resource already owns `/*`.
+Users can now choose either:
+
+- **Recommended**: `@Rest(mixins=BasicHealthResource.class)` on the root 
resource (single servlet).
+- **Fallback**: standalone `HealthProbeConfiguration` auto-mount (separate 
servlet, explicit paths).
+
 #### OpenAPI 3.1 Emission + `apiFormat` Knob (TODO-63)
 
 `juneau-rest-server` now ships first-class OpenAPI 3.1 document generation 
alongside the existing Swagger v2 path:
@@ -2327,6 +2360,53 @@ public class AccountResource {
 
 See [REST Server — RFC 7807 Problem 
Details](/docs/topics/RestServerProblemDetails) for the full topic, including 
worked examples for the resource-level opt-in, the per-op fanout, the 
`ProblemException` throw path, and `ProblemMapper` registration.
 
+#### Conditional-GET / ETag helpers
+
+`juneau-rest-server` now ships an RFC 7232 conditional-request layer so 
handlers no longer need to hand-roll `ETag` / `Last-Modified` / `If-None-Match` 
/ `If-Match` plumbing. The wiring is purely additive — existing handlers 
continue to work unchanged.
+
+##### Response-side setters on `RestResponse`
+
+Six fluent setters on <a 
href="/site/apidocs/org/apache/juneau/rest/RestResponse.html" 
target="_blank">RestResponse</a>:
+
+- `eTag(String)` / `eTag(EntityTag)` — sets the `ETag` response header. The 
string form delegates to `EntityTag.of(...)` so quoting (strong: `"v1"`; weak: 
`W/"v1"`) is validated up front.
+- `lastModified(Instant)` / `lastModified(ZonedDateTime)` — sets the 
`Last-Modified` response header in RFC 7231 IMF-fixdate format. Values are 
truncated to second precision and converted to UTC.
+- `cacheControl(String)` / `cacheControl(CacheControlBuilder)` — sets the 
`Cache-Control` response header. The builder form gives a typed API for the 
standard directives.
+
+##### Typed `Cache-Control` builder
+
+New <a 
href="/site/apidocs/org/apache/juneau/http/header/CacheControlBuilder.html" 
target="_blank">CacheControlBuilder</a> in `juneau-rest-common` offers a 
typo-resistant fluent API over the directive set: `publicCache()` / 
`privateCache()`, `noCache()` / `noStore()` / `noTransform()`, 
`mustRevalidate()` / `proxyRevalidate()`, `immutable()`, `maxAge(long)` / 
`maxAge(Duration)`, `sMaxAge(...)`, `staleWhileRevalidate(...)`, 
`staleIfError(...)`, and `extension(String)` for forward-compat di [...]
+
+##### Request-side `checkPreconditions(RestResponse)`
+
+New <a 
href="/site/apidocs/org/apache/juneau/rest/RestRequest.html#checkPreconditions-org.apache.juneau.rest.RestResponse-"
 target="_blank">RestRequest.checkPreconditions(RestResponse)</a> evaluates the 
four conditional request headers against the `ETag` / `Last-Modified` already 
set on the response, and returns an `Optional<BasicHttpException>`:
+
+- Empty → no precondition matched and the handler may proceed.
+- Non-empty → the contained exception (a `BasicHttpException` carrying `304 
Not Modified` or `412 Precondition Failed`) should be thrown to short-circuit.
+
+The implementation follows RFC 7232 §6 ordering exactly: `If-Match` → 
`If-Unmodified-Since` → `If-None-Match` → `If-Modified-Since`. ETag matching 
honors RFC 7232 §2.3.2 (strong comparison for `If-Match`; weak comparison for 
`If-None-Match`). `If-Modified-Since` is only consulted on `GET` / `HEAD`. 
Malformed dates are silently ignored per Postel's law.
+
+##### Typical handler shape
+
+```java
+@RestGet("/orders/{id}")
+public Order get(@Path long id, RestRequest req, RestResponse res) {
+    var order = repo.find(id);
+    res.eTag("\"" + order.version() + "\"")
+       .lastModified(order.updated())
+       
.cacheControl(CacheControlBuilder.create().publicCache().maxAge(60).build());
+    req.checkPreconditions(res).ifPresent(e -> { throw e; });
+    return order;
+}
+```
+
+##### Notes
+
+- The response's `ETag` / `Last-Modified` headers **must be set before** 
`checkPreconditions(...)` is called — the check reads from the already-set 
response headers so it can compare against the values the handler intends to 
send.
+- `If-Match` always uses strong comparison: a `W/"v1"` response ETag never 
matches an `If-Match`, even if the opaque tags are identical.
+- `If-None-Match` uses weak comparison: `"v1"` and `W/"v1"` both match. The 
wildcard `*` matches when any ETag is set on the response.
+
+See [REST Server — Conditional-GET / ETag 
Helpers](/docs/topics/RestServerConditionalGet) for the full topic, including 
the 16-cell precedence matrix, weak/strong ETag rules, and a worked round-trip 
example.
+
 ### juneau-rest-client
 
 #### REST session option wire helpers
@@ -2452,6 +2532,24 @@ See <a 
href="/docs/topics/MicroserviceCoreInject">Inject-Aware Microservice</a>
 
 ### juneau-microservice-jetty
 
+#### `HealthProbeConfiguration` (TODO-65)
+
+`juneau-microservice-jetty` now ships `HealthProbeConfiguration`, an opt-in 
`@Configuration` module
+that contributes:
+
+- `@Bean Servlet healthProbeServlet()` returning `BasicHealthResource` for 
auto-mount through
+  `JettyServerComponent`.
+- `@Bean HealthProbeSettings` defaulting to a `1s` indicator timeout 
(overridable via user bean).
+
+End-state bootstrap:
+
+```java
+Microservice.create()
+    .configurations(JettyConfiguration.class, HealthProbeConfiguration.class, 
AppConfig.class)
+    .build()
+    .start();
+```
+
 #### `JettyMicroservice` Replaced by `JettyConfiguration` (TODO-36, 
**BREAKING**)
 
 The `JettyMicroservice` subclass, `JettyMicroserviceListener` interface, and 
`BasicJettyMicroserviceListener`
diff --git a/pages/topics/10.07a.RestServerComposition.md 
b/pages/topics/10.07a.RestServerComposition.md
new file mode 100644
index 0000000000..35cbaf3fd6
--- /dev/null
+++ b/pages/topics/10.07a.RestServerComposition.md
@@ -0,0 +1,389 @@
+---
+title: "REST Server — Mixins and Multi-Mount Paths"
+slug: RestServerCompositionMixinsAndPaths
+---
+
+Juneau REST servers ship two complementary composition / mounting primitives 
on the class-level
+[`@Rest`](/site/apidocs/org/apache/juneau/rest/annotation/Rest.html) 
annotation:
+
+- **`@Rest(mixins=Class<?>[])`** — *composition without inheritance.* Graft 
every
+  `@RestOp`-group method (`@RestGet`, `@RestPost`, `@RestPut`, `@RestPatch`, 
`@RestDelete`,
+  `@RestOptions`, `@RestOp`) from a listed class into the importing resource's 
operation tree.
+  Local methods on the importing resource win on path/method collisions.
+- **`@Rest(paths=String[])`** — *multi-mount for top-level servlets.* Mount a 
single
+  `RestServlet` instance under multiple exact URL patterns. Primarily intended 
for back-compat-
+  friendly add-on endpoints (probes, well-known paths) that need to live at 
fixed URLs without
+  fighting an app's root `path="/"` mapping.
+
+Both attributes are purely additive — resources that don't declare them keep 
their pre-9.5.0
+behavior unchanged. They are also not mutually exclusive: a single class can 
declare both at once
+(`BasicHealthResource` ships with both supported), and downstream consumers 
pick whichever
+deployment style fits their app.
+
+## Motivation
+
+### Why composition?
+
+Java single-inheritance forces a binary choice when you want to pull in a 
built-in resource such
+as `BasicHealthResource`:
+
+- Subclass it — and lose the ability to subclass your own framework type 
(`BasicRestServlet`,
+  `BasicRestObjectGroup`, etc.).
+- Wire it as a separate child resource — and pay the cost of an extra servlet 
mount, a separate
+  `RestContext`, a separate bean store, and so on.
+
+`@Rest(mixins=...)` is the third option: keep your existing inheritance chain, 
but borrow the
+`@RestOp` methods from one (or several) addon classes into your own operation 
tree. The mixin's
+methods run inside *your* `RestContext`, share *your* bean store, and surface 
at *your*
+configured `path`. The cost of pulling in an addon drops to a single class 
literal on `@Rest`.
+
+### Why multi-mount?
+
+Servlet `path` is conventionally a single prefix (`/*`-style). Add-on 
endpoints whose URLs are
+fixed by external convention — Kubernetes probes (`/healthz`, `/readyz`, 
`/livez`), well-known
+URIs (`/.well-known/...`), bootstrap endpoints, etc. — don't fit that mold. 
Mounting them under
+a single `path="/"` collides with whatever the app's own root resource owns; 
mounting them under
+a long `path` prefix breaks the contract clients depend on.
+
+`@Rest(paths=...)` solves this: one servlet instance, multiple exact-match URL 
patterns, no
+prefix prefix-stripping. The `JettyServerComponent` auto-mount path honors 
`paths` when present
+and falls back to `path` otherwise.
+
+## `@Rest(mixins=...)`
+
+### What it does
+
+When the importing resource's `RestContext` is built, the mixin walk:
+
+1. Collects the transitive closure of all mixin classes declared on the 
importing class
+   (across `noInherit`-aware `@Rest` chains), in parent-to-child order, with a 
cycle guard.
+2. Instantiates each mixin class through the importing resource's bean store 
and caches the
+   instance in the bean store (one mixin instance per importing resource).
+3. Walks each mixin class's `@RestOp`-group methods *as if they were declared 
on the importing
+   resource*, registering them in the operation tree against the cached mixin 
instance as the
+   invocation target.
+4. Walks the importing resource's local `@RestOp`-group methods last. Local 
methods win on
+   path/method collisions — by registration order — so a resource can 
selectively override a
+   single mixin operation without losing the others.
+
+### Example: mixing in `BasicHealthResource`
+
+The recommended way to wire Kubernetes probes into an existing root servlet:
+
+```java
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.health.*;
+import org.apache.juneau.rest.servlet.*;
+
+@Rest(
+    path="/",
+    mixins=BasicHealthResource.class
+)
+public class RootResources extends BasicRestServlet {
+
+    @Bean(name="dbHealth")
+    HealthIndicator dbHealth() {
+        return () -> Health.up("db").detail("validationQueryMs", 12).build();
+    }
+}
+```
+
+After construction, the importing servlet exposes (in addition to its own 
routes):
+
+- `GET /healthz` — aggregated probe response (`200` / `503`).
+- `GET /readyz` — readiness-only aggregation.
+- `GET /livez` — liveness-only aggregation.
+
+No subclassing, no extra `ServletHolder`, no separate context. The mixin's 
three `@RestGet`
+methods run inside `RootResources`' own `RestContext` and share its bean store 
— which is why the
+`@Bean HealthIndicator dbHealth()` factory above is picked up by the mixin's 
aggregation logic
+without any extra registration.
+
+### Importer-wins on collision
+
+If the importing resource declares a route at the same `(path, method)` tuple 
as a mixin route,
+the importing resource's method wins:
+
+```java
+@Rest
+public class HealthMixin {
+    @RestGet(path="/same")
+    public String fromMixin() { return "mixin"; }
+}
+
+@Rest(mixins=HealthMixin.class)
+public class RootResources extends BasicRestServlet {
+    @RestGet(path="/same")
+    public String fromResource() { return "resource"; }
+}
+```
+
+A `GET /same` request resolves to `RootResources.fromResource()` and returns 
`"resource"`. The
+mixin's `fromMixin()` is reachable only if the importer removes its own 
`/same` route.
+
+This is the standard "decorator pattern but with importer precedence" — pull 
in everything by
+default, override per-route as needed.
+
+### Transitive mixins and cycle handling
+
+Mixins are walked transitively: if `A` mixes in `B`, and `B` mixes in `C`, 
then `A` ends up with
+the operations declared on `B` *and* on `C`. The walk is breadth-first, 
parent-to-child, and uses
+a `visited` set keyed by mixin class so each mixin contributes at most once 
even when reachable
+via multiple paths:
+
+```java
+@Rest
+public class C {
+    @RestGet(path="/c") public String c() { return "c"; }
+}
+
+@Rest(mixins=C.class)
+public class B {
+    @RestGet(path="/b") public String b() { return "b"; }
+}
+
+@Rest(mixins=B.class)
+public class A extends BasicRestServlet {
+    @RestGet(path="/root") public String root() { return "root"; }
+}
+```
+
+`A` exposes `/root`, `/b`, and `/c`. Multiple mixins on the same importer 
compose the same way:
+`@Rest(mixins={A_MixinA.class, B_MixinC.class})` pulls in everything from both 
walks, deduped by
+visited-set so a diamond doesn't double-register.
+
+Cycles are tolerated — the visited set short-circuits any back-edge — so a 
mixin class that
+transitively mixes in its own importer doesn't recurse infinitely.
+
+### Bean-store-resolved mixin instances
+
+Mixin instances are instantiated via the importing resource's bean store, so a 
mixin class with
+a constructor that takes injected dependencies wires up the same way a local 
resource class does.
+The instance is cached on first use and stored back into the bean store, so:
+
+- A subsequent `bs.getBean(MyMixin.class)` from elsewhere in the resource 
graph returns the
+  cached instance.
+- Subsequent calls to the same mixin route reuse that instance (no per-request 
instantiation).
+- A `@Bean` factory method on the importing resource for the mixin type, if 
present, takes
+  precedence over the default constructor walk.
+
+### Mixin invocation target
+
+`RestOpInvoker` ordinarily invokes the operation method on 
`RestSession.getResource()` — the
+importing resource instance. For mixin operations, the invocation target is 
explicitly the cached
+*mixin* instance (not the importing resource), so the mixin's method body can 
rely on `this`
+referring to the mixin and on the mixin's own private state being available. 
Both the regular
+and RRPC dispatch paths honor this distinction.
+
+## `@Rest(paths=...)`
+
+### What it does
+
+`paths` is an alternative to `path` for **top-level servlet mounting**. Where 
`path` declares a
+single prefix (typically `/*`-style — `path="/foo"` matches `/foo/*`), `paths` 
declares an array
+of **exact-match** URL patterns that all map to the same `ServletHolder`:
+
+```java
+@Rest(paths={"/healthz","/readyz","/livez"})
+public class BasicHealthResource extends BasicRestServlet { ... }
+```
+
+The Jetty auto-mount logic (in `JettyServerComponent`) sees the `paths` 
attribute and registers
+**one** servlet instance with the `ServletContextHandler` under **three** 
separate path-spec
+mappings (`/healthz`, `/readyz`, `/livez`). The same `BasicHealthResource` 
instance handles
+every request that lands on any of the three URLs.
+
+Path-spec semantics differ from `path`:
+
+| Attribute    | Pattern shape           | Servlet container interpretation    
          |
+|--------------|-------------------------|-----------------------------------------------|
+| `path="/foo"`| Prefix (`/foo/*`-style) | Matches `/foo`, `/foo/bar`, 
`/foo/baz/qux`, … |
+| `paths={"/foo"}` | Exact (`/foo`)      | Matches only `/foo` (no subpath 
wildcard).   |
+
+Use `path` when the resource owns a URL subtree (`/orders/*`). Use `paths` 
when the resource owns
+one (or several) specific URLs and you don't want a wildcard absorbing sibling 
routes.
+
+### Example: probe URLs without a root collision
+
+The motivating use case. `BasicHealthResource` ships with 
`@Rest(paths={"/healthz","/readyz","/livez"})`
+and is wired in via `HealthProbeConfiguration`:
+
+```java
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.microservice.jetty.*;
+import org.apache.juneau.rest.health.*;
+
+import jakarta.servlet.*;
+
+@Configuration
+public class AppConfig {
+
+    @Bean
+    Servlet root() {
+        return new RootResources();    // @Rest(path="/")
+    }
+
+    @Bean
+    HealthIndicator dbHealth() {
+        return () -> Health.up("db").detail("validationQueryMs", 12).build();
+    }
+}
+
+public class App {
+    public static void main(String[] args) throws Exception {
+        Microservice.create()
+            .args(args)
+            .configurations(JettyConfiguration.class, 
HealthProbeConfiguration.class, AppConfig.class)
+            .build()
+            .start()
+            .join();
+    }
+}
+```
+
+After startup, the Jetty container has two top-level mounts:
+
+| Path spec | Servlet                 | Source                       |
+|-----------|-------------------------|------------------------------|
+| `/*`      | `RootResources`         | `@Bean Servlet root()` + 
`@Rest(path="/")` |
+| `/healthz`| `BasicHealthResource`   | `@Bean Servlet healthProbeServlet()` + 
`@Rest(paths=...)` |
+| `/readyz` | `BasicHealthResource`   | (same instance, second mapping)  |
+| `/livez`  | `BasicHealthResource`   | (same instance, third mapping)   |
+
+The three probe paths are exact-match, so they're matched by the servlet 
container *before*
+falling through to `RootResources`'s `/*` prefix mount. No collision, no 
order-of-registration
+fragility — just three explicit URLs that always land on `BasicHealthResource`.
+
+### Single `ServletHolder`, multiple mappings
+
+Multi-mount uses **one** `ServletHolder` instance and registers it under each 
path-spec:
+
+```java
+var sh = new ServletHolder(servlet);
+for (var pathSpec : pathSpecs)
+    getServletContextHandler().addServlet(sh, pathSpec);
+```
+
+Consequences:
+
+- One servlet lifecycle (`init` / `destroy` are called once).
+- One backing `RestContext` (shared across all mount points).
+- Servlet-container `getServletPath()` reflects whichever path the request 
hit, but
+  `RestRequest` route matching runs against the resource's own operation tree 
regardless of
+  which mount fired.
+
+### Collision detection
+
+Each auto-mount path-spec is tracked against its declaring source, and the 
registration loop
+fails fast on collisions:
+
+```
+java.lang.RuntimeException: Servlet mount path collision: '/same' is already 
mounted by
+  @Bean org.example.FirstServlet[first]; refused by @Bean 
org.example.SecondServlet[second].
+```
+
+Both `path` and `paths` mounts flow through the same collision check, so:
+
+- Two resources declaring `@Rest(paths={"/healthz"})` fail at startup.
+- A resource declaring `@Rest(paths={"/healthz"})` and another declaring 
`@Rest(path="/healthz")`
+  do **not** collide — they map to different path-specs (`/healthz` exact vs 
`/healthz/*` prefix).
+- A `paths` array internally is just a list of exact-match path-specs; a 
duplicate inside the
+  array is still a collision (`paths={"/h","/h"}` fails fast).
+
+The collision message names both the prior mount source and the refused mount 
source, so
+multi-bean apps can find the conflict without digging through Jetty internals.
+
+### Interaction with `path`
+
+When both `path` and `paths` are present on the same resource, the Jetty 
auto-mount logic
+**prefers `paths`** for top-level servlet registration. The `path` attribute 
is still meaningful
+for child-resource composition — i.e. when the resource is referenced via 
`@Rest(children=...)`
+on a parent, the child's `path` is used as the subpath under the parent.
+
+This split lets a resource ship with sensible defaults for both deployment 
styles:
+
+- Top-level mount (`@Bean Servlet`): `paths` wins, the resource lands at its 
declared exact URLs.
+- Child mount (`@Rest(children=...)`): `path` wins, the resource lands under 
the parent's
+  subtree.
+
+## When to choose which
+
+| You want…                                                       | Use        
    |
+|-----------------------------------------------------------------|----------------|
+| Add an addon's `@RestOp` methods to an existing root servlet.   | `mixins`   
    |
+| Mount a separate addon servlet at fixed URLs without prefixes.  | `paths`    
    |
+| Override one route on a mixin while keeping the rest.           | `mixins` + 
local method on importer (importer-wins). |
+| Run an addon in isolation (its own context, bean store, etc.).  | `paths` — 
declare the addon as its own `@Bean Servlet`. |
+| Keep the option open and let downstream pick.                   | Declare 
both on the addon class. `BasicHealthResource` does this. |
+
+The two are not mutually exclusive. `BasicHealthResource` declares
+`@Rest(paths={"/healthz","/readyz","/livez"})` so it can be auto-mounted 
standalone (Option B in
+the probe docs), and downstream apps can additionally pull it in via
+`@Rest(mixins=BasicHealthResource.class)` on their own root resource (Option 
A). The mixin path
+ignores the `paths` array — mixin operations are grafted at the importer's URL 
space, not the
+mixin's own.
+
+## Caveats
+
+A few intentional limitations to be aware of.
+
+### Mixin scope is `@RestOp` methods only
+
+The mixin walk grafts `@RestOp`-group **methods** from the mixin class. It 
does **not** inherit:
+
+- The mixin's class-level `@Rest(serializers=..., parsers=..., encoders=..., 
guards=..., converters=..., ...)`
+  configuration. The importing resource's `@Rest` config applies to mixin 
operations — including
+  serializers, parsers, content negotiation, response processors, error 
handling, guards,
+  converters, and so on.
+- The mixin's class-level `@HtmlDocConfig` / `@JsonConfig` / other config 
annotations.
+- The mixin's `@Rest(children=...)` child resources. Mixins don't compose 
child trees.
+- The mixin's `@Rest(path=...)` or `@Rest(paths=...)` — the importing 
resource's path / paths
+  configuration is what controls mounting.
+- The mixin class's superclass methods (`extends BasicRestServlet`, etc.). 
Only the mixin
+  class's own declared `@RestOp` methods are grafted; the mixin's inheritance 
chain isn't
+  re-traversed by the walk. (The mixin class itself can still subclass 
anything it likes — the
+  superclass methods just don't pile into the importer.)
+- Static fields, instance fields, or per-mixin private state — unless the 
mixin's `@RestOp`
+  methods explicitly read them. The cached mixin instance is the invocation 
target, so any
+  per-instance state on the mixin is honored; it just isn't visible to the 
importing resource's
+  local methods or to other mixins.
+
+If you need the mixin's full config, the right tool is `@Rest(children=...)` 
(or an explicit
+servlet mount via `@Rest(paths=...)`), not `mixins`.
+
+### `paths` is exact-match only
+
+`paths` entries are normalized to exact-match servlet path-specs. There's no 
equivalent of
+`path`'s prefix-extension behavior. If you want `/healthz/extra/segment` to 
also resolve to the
+probe resource, declare it explicitly 
(`paths={"/healthz","/healthz/extra/segment"}`) or use
+`path="/healthz"` for the whole subtree.
+
+### `paths` is honored by the auto-mount path
+
+`paths` is interpreted by `JettyServerComponent`'s `@Bean Servlet` auto-mount 
loop. Manual
+`addServlet(...)` calls and externally-configured `Jetty/servlets` / 
`Jetty/servletMap` entries
+take whatever path-spec the caller hands them, regardless of the `paths` 
attribute on the
+servlet class. The auto-mount honor is opt-in — `@Bean Servlet` is the trigger.
+
+### Mixin operations don't surface in Swagger as "imported"
+
+The Swagger / OpenAPI emission walks the importing resource's full operation 
tree, so mixin
+operations appear in the generated spec as ordinary operations of the 
importing resource. There
+is no per-operation "imported from X" tag in the spec. If you need that 
distinction for API
+consumers, declare the addon as a separate child resource 
(`@Rest(children=...)`) so it gets its
+own Swagger / OpenAPI scope.
+
+## See also
+
+- [Health / Readiness / Liveness Probes](/docs/topics/HealthProbes) — the 
canonical consumer of
+  both primitives, with full Option A (mixin) and Option B (standalone 
auto-mount) examples.
+- [@Rest-Annotated Class Basics](/docs/topics/RestAnnotatedClassBasics) — the 
broader `@Rest`
+  attribute reference.
+- [Child Resources](/docs/topics/ChildResources) — the alternative composition 
primitive when
+  you want a separate `RestContext` and bean store per addon.
+- [Path Patterns](/docs/topics/PathPatterns) — operation-level path matching 
(the layer mixin
+  routes are grafted *into*).
+- [REST Server Overview](/docs/topics/RestServerOverview) — where these 
primitives sit in the
+  overall request-routing pipeline.
diff --git a/pages/topics/10.20b.RestServerConditionalGet.md 
b/pages/topics/10.20b.RestServerConditionalGet.md
new file mode 100644
index 0000000000..fc1bb2bdb7
--- /dev/null
+++ b/pages/topics/10.20b.RestServerConditionalGet.md
@@ -0,0 +1,232 @@
+---
+title: "Conditional-GET / ETag Helpers"
+slug: RestServerConditionalGet
+---
+
+Juneau REST servers ship a minimal but complete [RFC 7232 — Conditional 
Requests](https://www.rfc-editor.org/rfc/rfc7232) helper layer so handlers no 
longer need to hand-roll `ETag` / `Last-Modified` / `If-None-Match` / 
`If-Match` plumbing. The feature couples three pieces:
+
+- A handful of fluent setters on <a 
href="/site/apidocs/org/apache/juneau/rest/RestResponse.html" 
target="_blank">RestResponse</a> — `eTag(...)`, `lastModified(...)`, 
`cacheControl(...)`.
+- A typed <a 
href="/site/apidocs/org/apache/juneau/http/header/CacheControlBuilder.html" 
target="_blank">CacheControlBuilder</a> in `juneau-rest-common` for 
typo-resistant `Cache-Control` directive construction.
+- A `checkPreconditions(...)` helper on <a 
href="/site/apidocs/org/apache/juneau/rest/RestRequest.html" 
target="_blank">RestRequest</a> that evaluates the four conditional request 
headers in the exact order RFC 7232 §6 specifies, and returns an 
`Optional<BasicHttpException>` carrying the right `304 Not Modified` or `412 
Precondition Failed` for the caller to throw.
+
+The wiring is purely additive — existing handlers continue to work unchanged.
+
+## Motivation
+
+A textbook conditional-GET handler looks like this:
+
+```java
+@RestGet("/{id}")
+public Order get(@Path long id, RestRequest req, RestResponse res) {
+    var order = repo.find(id);
+    var tag = "\"" + order.version() + "\"";
+    res.eTag(tag).lastModified(order.updated());
+    req.checkPreconditions(res).ifPresent(e -> { throw e; });
+    return order;
+}
+```
+
+Without the helpers, every handler that wants conditional-GET support has to:
+
+1. Format `ETag` / `Last-Modified` headers correctly (quoting, IMF-fixdate, 
UTC).
+2. Read four request headers (`If-Match`, `If-Unmodified-Since`, 
`If-None-Match`, `If-Modified-Since`).
+3. Implement RFC 7232 §6 ordering (which header wins when more than one is 
present).
+4. Implement RFC 7232 §2.3.2 ETag matching rules (strong vs weak comparison).
+5. Pick the right status code (`304` vs `412`) and short-circuit cleanly.
+
+That's a lot of fiddly RFC bookkeeping. The helpers compress all of it into a 
one-line call.
+
+## Response-side setters on `RestResponse`
+
+Six fluent setters, all chainable:
+
+| Method                              | Sets                | Notes |
+|-------------------------------------|---------------------|-------|
+| `eTag(String)`                      | `ETag`              | Delegates to 
`EntityTag.of(...)` so quoting (strong: `"v1"`; weak: `W/"v1"`) is validated up 
front. |
+| `eTag(EntityTag)`                   | `ETag`              | When the caller 
already has a typed `EntityTag`. |
+| `lastModified(Instant)`             | `Last-Modified`     | Truncated to 
second precision; formatted as RFC 7231 IMF-fixdate in UTC. |
+| `lastModified(ZonedDateTime)`       | `Last-Modified`     | Converted to UTC 
before formatting. |
+| `cacheControl(String)`              | `Cache-Control`     | Raw header value 
(e.g. `"public, max-age=3600"`). |
+| `cacheControl(CacheControlBuilder)` | `Cache-Control`     | Typed builder 
form — see below. |
+
+```java
+res.eTag("\"v42\"")
+   .lastModified(Instant.parse("2026-05-22T00:00:00Z"))
+   .cacheControl("public, max-age=60");
+```
+
+Produces:
+
+```http
+ETag: "v42"
+Last-Modified: Fri, 22 May 2026 00:00:00 GMT
+Cache-Control: public, max-age=60
+```
+
+## `CacheControlBuilder` — typed directive construction
+
+<a href="/site/apidocs/org/apache/juneau/http/header/CacheControlBuilder.html" 
target="_blank">CacheControlBuilder</a> (in `juneau-rest-common`) is a 
typo-resistant fluent API over the standard directive set:
+
+| Method                              | Directive                       |
+|-------------------------------------|---------------------------------|
+| `publicCache()` / `privateCache()`  | `public` / `private`            |
+| `noCache()`                         | `no-cache`                      |
+| `noStore()`                         | `no-store`                      |
+| `noTransform()`                     | `no-transform`                  |
+| `mustRevalidate()`                  | `must-revalidate`               |
+| `proxyRevalidate()`                 | `proxy-revalidate`              |
+| `immutable()`                       | `immutable`                     |
+| `maxAge(long)` / `maxAge(Duration)` | `max-age=N`                     |
+| `sMaxAge(long)` / `sMaxAge(Duration)` | `s-maxage=N`                  |
+| `staleWhileRevalidate(long)`        | `stale-while-revalidate=N`      |
+| `staleIfError(long)`                | `stale-if-error=N`              |
+| `extension(String)`                 | Arbitrary extension directive   |
+
+Directives are emitted in a stable order — cacheability (`public` / `private`) 
first, then boolean directives in source order (`no-cache` / `no-store` / 
`no-transform` / `must-revalidate` / `proxy-revalidate` / `immutable`), then 
numeric directives (`max-age` / `s-maxage` / `stale-while-revalidate` / 
`stale-if-error`), then extensions. This makes snapshot-based testing 
deterministic.
+
+```java
+res.cacheControl(CacheControlBuilder.create()
+    .publicCache()
+    .maxAge(Duration.ofMinutes(5))
+    .mustRevalidate()
+    .build());
+// → Cache-Control: public, must-revalidate, max-age=300
+```
+
+## Request-side `checkPreconditions(RestResponse)`
+
+<a 
href="/site/apidocs/org/apache/juneau/rest/RestRequest.html#checkPreconditions-org.apache.juneau.rest.RestResponse-"
 target="_blank">RestRequest.checkPreconditions(RestResponse)</a> evaluates the 
four conditional request headers against the `ETag` / `Last-Modified` already 
set on the response and returns an `Optional<BasicHttpException>`:
+
+| Return                                | Meaning                              
                                    |
+|---------------------------------------|--------------------------------------------------------------------------|
+| `Optional.empty()`                    | No precondition matched — handler 
may proceed.                           |
+| `Optional<BasicHttpException>` (304)  | A `BasicHttpException` carrying `304 
Not Modified` — throw to short-circuit. |
+| `Optional<BasicHttpException>` (412)  | A `BasicHttpException` carrying `412 
Precondition Failed` — throw to short-circuit. |
+
+The handler then chooses how to react:
+
+```java
+// Optional.ifPresent — short-circuit by throwing the carried exception:
+req.checkPreconditions(res).ifPresent(e -> { throw e; });
+
+// Or unwrap and re-shape the response yourself:
+var opt = req.checkPreconditions(res);
+if (opt.isPresent()) {
+    var ex = opt.get();
+    res.setStatus(ex.getStatusCode());
+    return null;
+}
+```
+
+### Ordering rule
+
+`checkPreconditions(...)` reads from the response's **already-set** `ETag` and 
`Last-Modified` headers. Set those before calling the check, so the helper 
compares against the values the handler intends to send:
+
+```java
+res.eTag("\"v42\"").lastModified(order.updated());   // 1. tag the response 
first
+req.checkPreconditions(res).ifPresent(e -> { throw e; });   // 2. then check
+return order;                                                 // 3. handler 
body
+```
+
+Setting the headers after the check produces stale comparisons.
+
+### RFC 7232 §6 precedence
+
+The helper applies the four conditional headers in the order RFC 7232 mandates:
+
+1. **`If-Match`** — strong comparison. On mismatch → `412 Precondition 
Failed`. If present, `If-Unmodified-Since` is ignored.
+2. **`If-Unmodified-Since`** — only consulted when `If-Match` is absent. If 
the resource's `Last-Modified` is strictly later than the supplied date → `412 
Precondition Failed`.
+3. **`If-None-Match`** — weak comparison. On match → `304 Not Modified` (for 
safe methods) or `412 Precondition Failed` (for unsafe methods). If present, 
`If-Modified-Since` is ignored.
+4. **`If-Modified-Since`** — only consulted when `If-None-Match` is absent 
**and** the method is `GET` or `HEAD`. If the resource's `Last-Modified` is not 
strictly later than the supplied date → `304 Not Modified`.
+
+### ETag matching rules
+
+| Comparison        | Used by             | Rule                               
                                 |
+|-------------------|---------------------|---------------------------------------------------------------------|
+| **Strong**        | `If-Match`          | Both ETags must be strong (no `W/` 
prefix) and have identical opaque tags. A weak response ETag never matches an 
`If-Match`. |
+| **Weak**          | `If-None-Match`     | Tags match if their opaque parts 
are identical, regardless of weak/strong markers. |
+| **Wildcard `*`**  | both                | `If-Match: *` matches when the 
response has any ETag (else `412`). `If-None-Match: *` matches when any ETag is 
set on the response (else passes through). |
+
+### Malformed dates
+
+`If-Modified-Since` / `If-Unmodified-Since` values that cannot be parsed as 
HTTP-date are silently ignored — they are treated as if the header were absent. 
This follows Postel's law and matches what mainstream servers (nginx, Apache 
httpd, Tomcat) do in practice.
+
+## Worked example — round trip
+
+A trivial repository-backed resource that demonstrates the full lifecycle:
+
+```java
+@Rest(path="/orders")
+public class OrderResource {
+
+    @RestGet("/{id}")
+    public Order get(@Path long id, RestRequest req, RestResponse res) {
+        var order = repo.find(id);
+        res.eTag("\"" + order.version() + "\"")
+           .lastModified(order.updated())
+           
.cacheControl(CacheControlBuilder.create().publicCache().maxAge(60).build());
+        req.checkPreconditions(res).ifPresent(e -> { throw e; });
+        return order;
+    }
+
+    @RestPut("/{id}")
+    public Order put(@Path long id, @Content Order in, RestRequest req, 
RestResponse res) {
+        var current = repo.find(id);
+        res.eTag("\"" + current.version() + 
"\"").lastModified(current.updated());
+        req.checkPreconditions(res).ifPresent(e -> { throw e; });   // 412 if 
If-Match is stale
+        var next = repo.save(in.withVersion(current.version() + 1));
+        res.eTag("\"" + next.version() + "\"").lastModified(next.updated());
+        return next;
+    }
+}
+```
+
+Wire-level interaction:
+
+```http
+# 1. First GET — full response with ETag + Last-Modified
+GET /orders/1
+→ 200 OK
+  ETag: "1"
+  Last-Modified: Fri, 22 May 2026 00:00:00 GMT
+  Cache-Control: public, max-age=60
+
+# 2. Re-fetch using If-None-Match — short-circuits to 304
+GET /orders/1
+If-None-Match: "1"
+→ 304 Not Modified
+
+# 3. PUT with stale If-Match — rejected
+PUT /orders/1
+If-Match: "0"
+{ ... }
+→ 412 Precondition Failed
+
+# 4. PUT with correct If-Match — succeeds and bumps ETag
+PUT /orders/1
+If-Match: "1"
+{ ... }
+→ 200 OK
+  ETag: "2"
+  Last-Modified: Sat, 23 May 2026 00:00:00 GMT
+```
+
+## Scope notes
+
+The helpers cover the wire-level conditional-GET layer. Out of scope for this 
release:
+
+- **Server-side response caching.** The helpers turn an `If-None-Match` match 
into a `304`, but the handler still computes the resource. A separate 
response-cache layer (compute-once / re-serve) is a future work item.
+- **Weak vs strong ETag policy.** <a 
href="/site/apidocs/org/apache/juneau/http/header/EntityTag.html" 
target="_blank">EntityTag</a> already models both via `isWeak()`; the helpers 
honor whatever the caller produced.
+- **`Vary` header automation.** Callers still set `Vary` explicitly when 
appropriate.
+- **`If-Range` / `206 Partial Content`.** Range-request support is a future 
work item.
+
+## See also
+
+- [REST Server — HTTP Status Codes](/docs/topics/HttpStatusCodes) — the 
framework defaults for non-OK statuses, including `304` and `412`.
+- [Java Method Throwable Types](/docs/topics/JavaMethodThrowableTypes) — what 
exceptions Juneau handlers can throw and how they map to HTTP statuses.
+
+## Resources
+
+- [RFC 7232 — Conditional Requests](https://www.rfc-editor.org/rfc/rfc7232)
+- [RFC 7234 — Caching](https://www.rfc-editor.org/rfc/rfc7234) (obsoleted by 
RFC 9111)
+- [RFC 9111 — HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111)
diff --git a/pages/topics/15.10.HealthProbes.md 
b/pages/topics/15.10.HealthProbes.md
new file mode 100644
index 0000000000..79d576ca55
--- /dev/null
+++ b/pages/topics/15.10.HealthProbes.md
@@ -0,0 +1,156 @@
+---
+title: "Health / Readiness / Liveness Probes"
+slug: HealthProbes
+---
+
+> **See also:** [REST Server — Mixins and Multi-Mount 
Paths](/docs/topics/RestServerCompositionMixinsAndPaths) — the underlying 
`@Rest(mixins=...)` and `@Rest(paths=...)` composition primitives this page is 
built on.
+
+Starting with **9.5.0**, Juneau provides an opt-in probe surface for Jetty 
microservices:
+
+- `GET /healthz`
+- `GET /readyz`
+- `GET /livez`
+
+Probe routing now has two supported integration styles:
+
+- **Preferred:** mix probe operations into your existing root resource with
+  `@Rest(mixins=BasicHealthResource.class)`.
+- **Fallback:** standalone probe servlet auto-mounted at explicit 
`@Rest(paths={...})` path specs
+  (`/healthz`, `/readyz`, `/livez`) via `HealthProbeConfiguration`.
+
+This avoids root-path collisions when your app already owns `@Rest(path="/")`.
+
+The endpoints aggregate every `HealthIndicator` bean from the microservice 
bean store and return an
+Actuator-style payload:
+
+```json
+{
+  "status": "UP",
+  "components": {
+    "dbHealth": {
+      "status": "UP",
+      "details": {
+        "validationQueryMs": 12
+      }
+    }
+  }
+}
+```
+
+HTTP status is:
+
+- **`200 OK`** when no component is `DOWN`.
+- **`503 Service Unavailable`** when any component is `DOWN`.
+
+## Quick start
+
+### Option A (recommended): mix into existing root resource
+
+```java
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.microservice.jetty.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.health.*;
+import org.apache.juneau.rest.servlet.*;
+
+@Rest(
+    path="/",
+    mixins=BasicHealthResource.class
+)
+public class RootResources extends BasicRestServlet {
+    @Bean(name="dbHealth")
+    HealthIndicator dbHealth() {
+        return () -> Health.up("db").detail("validationQueryMs", 12).build();
+    }
+}
+
+public class App {
+    public static void main(String[] args) throws Exception {
+        Microservice.create()
+            .args(args)
+            .configurations(JettyConfiguration.class)
+            .build()
+            .start()
+            .join();
+    }
+}
+```
+
+### Option B: standalone auto-mount (escape hatch)
+
+```java
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.microservice.jetty.*;
+import org.apache.juneau.rest.health.*;
+
+import jakarta.servlet.*;
+
+@Configuration
+public class AppConfig {
+
+    @Bean
+    Servlet root() {
+        return new RootResources();
+    }
+
+    @Bean
+    HealthIndicator dbHealth() {
+        return () -> Health.up("db").detail("validationQueryMs", 12).build();
+    }
+}
+
+public class App {
+    public static void main(String[] args) throws Exception {
+        Microservice.create()
+            .args(args)
+            .configurations(JettyConfiguration.class, 
HealthProbeConfiguration.class, AppConfig.class)
+            .build()
+            .start()
+            .join();
+    }
+}
+```
+
+`HealthProbeConfiguration` contributes a `Servlet` bean 
(`BasicHealthResource`) and Jetty mounts it
+at the three explicit path specs declared on the resource via 
`@Rest(paths={...})`.
+
+## `HealthIndicator` SPI
+
+`HealthIndicator` is a functional interface:
+
+```java
+@FunctionalInterface
+public interface HealthIndicator {
+    Health check();
+}
+```
+
+Return values are created with `Health.up(name)`, `Health.down(name, 
throwable)`, and
+`Health.unknown(name)`, with optional `.detail(key, value)` entries.
+
+Indicators can scope themselves to probe types by overriding `probes()` and 
returning a set of
+`HealthProbe` values:
+
+- `HealthProbe.LIVE`
+- `HealthProbe.READY`
+- `HealthProbe.STARTUP`
+
+By default, indicators participate in both liveness and readiness probes.
+
+## Timeout behavior
+
+Each indicator runs with a timeout from `HealthProbeSettings` (default: 1 
second). If a check times
+out or throws, the component is marked `DOWN` and the error is surfaced in 
`details.error`.
+
+Override defaults by contributing your own settings bean:
+
+```java
+@Bean
+HealthProbeSettings probeSettings() {
+    return HealthProbeSettings.create()
+        .timeout(Duration.ofSeconds(2))
+        .build();
+}
+```
diff --git a/pages/topics/23.01.V9.5-migration-guide.md 
b/pages/topics/23.01.V9.5-migration-guide.md
index 1eae1d6893..19d965a620 100644
--- a/pages/topics/23.01.V9.5-migration-guide.md
+++ b/pages/topics/23.01.V9.5-migration-guide.md
@@ -30,6 +30,12 @@ teams jumping from 9.1 (or earlier) directly to 9.5 have a 
single reference.
 | Custom annotation appliers — user code that subclassed the internal 
`AnnotationApplier<Rest, RestContext.Builder>` (or `AnnotationApplier<RestOp, 
RestOpContext.Builder>`) to extend the annotation-processing pass (the 
`apply(AnnotationInfo<A>, B builder)` hook invoked once per annotation during 
context construction). | **Removed.** The builder-based apply-pass is gone; 
`RestAnnotation.Apply` (`RestContextApply`) is now a package-private nested 
class inside `RestContext` and is not exten [...]
 | Custom `RestAnnotation.create(...)` / `RestOpAnnotation.create(...)` 
builder-of-builders patterns — programmatic construction of `@Rest` / `@RestOp` 
annotation proxies used to feed synthetic annotations into the builder 
apply-pass (common in test fixtures and extension libraries). | The annotation 
proxy builders still exist for test use (`RestAnnotation.create()` / 
`RestOpAnnotation.create()` are still available via annotation-test helpers), 
but they no longer feed into a builder apply [...]
 
+## Health Probe Routing (`mixins` + `paths`)
+
+| Old | New | Notes |
+|-----|-----|-------|
+| `BasicHealthResource` standalone auto-mount used `@Rest(path="/")`, which 
collided with root resources mounted at `/*` in Jetty microservices. | 
`BasicHealthResource` now uses `@Rest(paths={"/healthz","/readyz","/livez"})` 
and can also be composed into an existing root resource via 
`@Rest(mixins=BasicHealthResource.class)`. | Prefer `mixins` when you already 
have a root `@Rest(path="/")` resource. Keep standalone 
`HealthProbeConfiguration` only when you explicitly want a separate servl [...]
+
 ## SVL and Runtime Input Types Moved to `juneau-commons` (TODO-14)
 
 | Old | New | Notes |
diff --git a/sidebars.ts b/sidebars.ts
index 8c1be7b83b..9c976e4a01 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1327,6 +1327,11 @@ const sidebars: SidebarsConfig = {
                                                        id: 
'topics/10.07.HandlingFormPosts',
                                                        label: '10.7. Handling 
Form Posts',
                                                },
+                                               {
+                                                       type: 'doc',
+                                                       id: 
'topics/10.07a.RestServerComposition',
+                                                       label: '10.7a. Mixins 
and Multi-Mount Paths',
+                                               },
                                                {
                                                        type: 'doc',
                                                        id: 
'topics/10.08.RestServerSse',
@@ -1481,6 +1486,11 @@ const sidebars: SidebarsConfig = {
                                                        id: 
'topics/10.20a.RestServerProblemDetails',
                                                        label: '10.20a. RFC 
7807 / 9457 Problem Details',
                                                },
+                                               {
+                                                       type: 'doc',
+                                                       id: 
'topics/10.20b.RestServerConditionalGet',
+                                                       label: '10.20b. 
Conditional-GET / ETag Helpers',
+                                               },
                                                {
                                                        type: 'doc',
                                                        id: 
'topics/10.21.BuiltInParameters',
@@ -1847,6 +1857,11 @@ const sidebars: SidebarsConfig = {
                                                        id: 
'topics/15.09.Extending',
                                                        label: '15.9. 
Customizing via @Bean',
                                                },
+                                               {
+                                                       type: 'doc',
+                                                       id: 
'topics/15.10.HealthProbes',
+                                                       label: '15.10. Health / 
Readiness / Liveness Probes',
+                                               },
                                        ],
                                },
                                {


Reply via email to