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

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


The following commit(s) were added to refs/heads/master by this push:
     new 2c2fbd1167 Complete TODO-64 conditional request helpers
2c2fbd1167 is described below

commit 2c2fbd1167aa06b19bba4a2673ecc13178edb9be
Author: James Bognar <[email protected]>
AuthorDate: Fri May 22 21:05:45 2026 -0400

    Complete TODO-64 conditional request helpers
---
 .../juneau/http/header/CacheControlBuilder.java    | 328 +++++++++++++++++++
 .../java/org/apache/juneau/rest/RestRequest.java   | 157 +++++++++
 .../java/org/apache/juneau/rest/RestResponse.java  | 129 ++++++++
 .../juneau/http/header/NamedHeaders_Test.java      |   2 +
 .../juneau/rest/RestEtag_RoundTrip_Test.java       | 115 +++++++
 .../rest/RestRequest_CheckPreconditions_Test.java  | 332 +++++++++++++++++++
 .../juneau/rest/RestResponse_EtagHelpers_Test.java | 364 +++++++++++++++++++++
 ...=> FINISHED-64-etag-conditional-get-helpers.md} |   4 +-
 todo/TODO.md                                       |   2 -
 9 files changed, 1430 insertions(+), 3 deletions(-)

diff --git 
a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/header/CacheControlBuilder.java
 
b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/header/CacheControlBuilder.java
new file mode 100644
index 0000000000..69a119d404
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/header/CacheControlBuilder.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.http.header;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
+
+import java.time.*;
+import java.util.*;
+
+/**
+ * Fluent builder for assembling an HTTP <c>Cache-Control</c> header value.
+ *
+ * <p>
+ * Composes the directives defined by
+ * <a class='doclink' 
href='https://www.rfc-editor.org/rfc/rfc9111#name-cache-control'>RFC 9111 
§5.2</a> into the
+ * comma-separated wire format expected by the {@code Cache-Control} response 
header. Mutually exclusive
+ * cacheability directives (e.g. {@link #publicCache()} vs {@link 
#privateCache()}) overwrite one another;
+ * boolean directives (e.g. {@link #noStore()}) are idempotent.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ *     String <jv>value</jv> = CacheControlBuilder.<jsm>create</jsm>()
+ *             .publicCache()
+ *             .maxAge(3600)
+ *             .mustRevalidate()
+ *             .build();
+ *     <jc>// =&gt; "public, max-age=3600, must-revalidate"</jc>
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='jc'>{@link CacheControl}
+ *     <li class='extlink'><a class='doclink' 
href='https://www.rfc-editor.org/rfc/rfc9111'>RFC 9111 - HTTP Caching</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+public class CacheControlBuilder {
+
+       private static final String ARG_value = "value";
+
+       private String cacheability;        // "public" or "private" or null
+       private boolean noCache;
+       private boolean noStore;
+       private boolean noTransform;
+       private boolean mustRevalidate;
+       private boolean proxyRevalidate;
+       private boolean immutable;
+       private Long maxAge;
+       private Long sMaxAge;
+       private Long staleWhileRevalidate;
+       private Long staleIfError;
+       private final List<String> extensions = new ArrayList<>();
+
+       /**
+        * Creates a new empty builder.
+        *
+        * @return A new builder. Never <jk>null</jk>.
+        */
+       public static CacheControlBuilder create() {
+               return new CacheControlBuilder();
+       }
+
+       /**
+        * Adds the <c>public</c> directive (responses may be cached by any 
cache).
+        *
+        * <p>
+        * Clears any previously set <c>private</c> directive.
+        *
+        * @return This object.
+        */
+       public CacheControlBuilder publicCache() {
+               cacheability = "public";
+               return this;
+       }
+
+       /**
+        * Adds the <c>private</c> directive (responses may only be cached by 
the originating client).
+        *
+        * <p>
+        * Clears any previously set <c>public</c> directive.
+        *
+        * @return This object.
+        */
+       public CacheControlBuilder privateCache() {
+               cacheability = "private";
+               return this;
+       }
+
+       /**
+        * Adds the <c>no-cache</c> directive.
+        *
+        * <p>
+        * Forces caches to revalidate with the origin server before reusing a 
stored response.
+        *
+        * @return This object.
+        */
+       public CacheControlBuilder noCache() {
+               noCache = true;
+               return this;
+       }
+
+       /**
+        * Adds the <c>no-store</c> directive.
+        *
+        * <p>
+        * Forbids caches from storing any part of the request or response.
+        *
+        * @return This object.
+        */
+       public CacheControlBuilder noStore() {
+               noStore = true;
+               return this;
+       }
+
+       /**
+        * Adds the <c>no-transform</c> directive.
+        *
+        * <p>
+        * Forbids intermediaries from transforming the response payload.
+        *
+        * @return This object.
+        */
+       public CacheControlBuilder noTransform() {
+               noTransform = true;
+               return this;
+       }
+
+       /**
+        * Adds the <c>must-revalidate</c> directive.
+        *
+        * <p>
+        * Forces caches to revalidate stale responses with the origin before 
serving them.
+        *
+        * @return This object.
+        */
+       public CacheControlBuilder mustRevalidate() {
+               mustRevalidate = true;
+               return this;
+       }
+
+       /**
+        * Adds the <c>proxy-revalidate</c> directive (must-revalidate for 
shared caches only).
+        *
+        * @return This object.
+        */
+       public CacheControlBuilder proxyRevalidate() {
+               proxyRevalidate = true;
+               return this;
+       }
+
+       /**
+        * Adds the <c>immutable</c> directive.
+        *
+        * <p>
+        * Signals that the response body will not change for the duration of 
its freshness lifetime.
+        *
+        * @return This object.
+        */
+       public CacheControlBuilder immutable() {
+               immutable = true;
+               return this;
+       }
+
+       /**
+        * Adds the <c>max-age=N</c> directive (seconds).
+        *
+        * @param seconds Maximum freshness lifetime in seconds. Must be 
non-negative.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code seconds} is negative.
+        */
+       public CacheControlBuilder maxAge(long seconds) {
+               if (seconds < 0)
+                       throw illegalArg("max-age must be non-negative: {0}", 
seconds);
+               maxAge = seconds;
+               return this;
+       }
+
+       /**
+        * Adds the <c>max-age=N</c> directive from a {@link Duration}.
+        *
+        * @param value Maximum freshness lifetime. Must not be <jk>null</jk> 
or negative.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code value} is <jk>null</jk> 
or negative.
+        */
+       public CacheControlBuilder maxAge(Duration value) {
+               assertArgNotNull(ARG_value, value);
+               return maxAge(value.getSeconds());
+       }
+
+       /**
+        * Adds the <c>s-maxage=N</c> directive (shared-cache max-age in 
seconds).
+        *
+        * @param seconds Maximum freshness lifetime in seconds for shared 
caches. Must be non-negative.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code seconds} is negative.
+        */
+       public CacheControlBuilder sMaxAge(long seconds) {
+               if (seconds < 0)
+                       throw illegalArg("s-maxage must be non-negative: {0}", 
seconds);
+               sMaxAge = seconds;
+               return this;
+       }
+
+       /**
+        * Adds the <c>s-maxage=N</c> directive from a {@link Duration}.
+        *
+        * @param value Maximum freshness lifetime for shared caches. Must not 
be <jk>null</jk> or negative.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code value} is <jk>null</jk> 
or negative.
+        */
+       public CacheControlBuilder sMaxAge(Duration value) {
+               assertArgNotNull(ARG_value, value);
+               return sMaxAge(value.getSeconds());
+       }
+
+       /**
+        * Adds the <c>stale-while-revalidate=N</c> directive (seconds).
+        *
+        * @param seconds Number of seconds a cache may serve a stale response 
while asynchronously revalidating.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code seconds} is negative.
+        */
+       public CacheControlBuilder staleWhileRevalidate(long seconds) {
+               if (seconds < 0)
+                       throw illegalArg("stale-while-revalidate must be 
non-negative: {0}", seconds);
+               staleWhileRevalidate = seconds;
+               return this;
+       }
+
+       /**
+        * Adds the <c>stale-if-error=N</c> directive (seconds).
+        *
+        * @param seconds Number of seconds a cache may serve a stale response 
when the origin is unreachable.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code seconds} is negative.
+        */
+       public CacheControlBuilder staleIfError(long seconds) {
+               if (seconds < 0)
+                       throw illegalArg("stale-if-error must be non-negative: 
{0}", seconds);
+               staleIfError = seconds;
+               return this;
+       }
+
+       /**
+        * Adds an arbitrary cache-control extension directive (e.g. 
<c>"community=\"UCI\""</c>).
+        *
+        * <p>
+        * Extensions are appended in registration order at the end of the 
directive list. The caller is responsible for
+        * supplying a token that conforms to the <c>cache-directive</c> 
grammar of RFC 9111 §5.2.3.
+        *
+        * @param value The extension token. Must not be <jk>null</jk> or blank.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code value} is <jk>null</jk> 
or blank.
+        */
+       public CacheControlBuilder extension(String value) {
+               assertArgNotNull(ARG_value, value);
+               var v = value.trim();
+               if (v.isEmpty())
+                       throw illegalArg("cache-control extension must not be 
blank");
+               extensions.add(v);
+               return this;
+       }
+
+       /**
+        * Builds the comma-separated <c>Cache-Control</c> header value.
+        *
+        * <p>
+        * Returns an empty string when no directives have been registered.
+        *
+        * @return The header value. Never <jk>null</jk>.
+        */
+       public String build() {
+               var parts = new ArrayList<String>(12);
+               if (cacheability != null)
+                       parts.add(cacheability);
+               if (noCache)
+                       parts.add("no-cache");
+               if (noStore)
+                       parts.add("no-store");
+               if (noTransform)
+                       parts.add("no-transform");
+               if (mustRevalidate)
+                       parts.add("must-revalidate");
+               if (proxyRevalidate)
+                       parts.add("proxy-revalidate");
+               if (immutable)
+                       parts.add("immutable");
+               if (maxAge != null)
+                       parts.add("max-age=" + maxAge);
+               if (sMaxAge != null)
+                       parts.add("s-maxage=" + sMaxAge);
+               if (staleWhileRevalidate != null)
+                       parts.add("stale-while-revalidate=" + 
staleWhileRevalidate);
+               if (staleIfError != null)
+                       parts.add("stale-if-error=" + staleIfError);
+               parts.addAll(extensions);
+               return String.join(", ", parts);
+       }
+
+       /**
+        * Builds a {@link CacheControl} header bean directly from this builder.
+        *
+        * @return A {@link CacheControl} carrying {@link #build()}. Never 
<jk>null</jk>.
+        */
+       public CacheControl toHeader() {
+               return CacheControl.of(build());
+       }
+
+       @Override
+       public String toString() {
+               return build();
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
index cd53cbcb1d..d1426cc1ba 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
@@ -19,7 +19,9 @@ package org.apache.juneau.rest;
 import org.apache.juneau.commons.bean.BeanMeta;
 import org.apache.juneau.commons.http.MediaType;
 import org.apache.juneau.commons.http.StringRanges;
+import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
 import static java.util.Optional.*;
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
 import static org.apache.juneau.rest.RestSharedConstants.*;
 import static org.apache.juneau.commons.utils.IoUtils.*;
@@ -34,6 +36,9 @@ import java.lang.reflect.Proxy;
 import java.net.*;
 import java.nio.charset.*;
 import java.text.*;
+import java.time.*;
+import java.time.format.*;
+import java.time.temporal.*;
 import java.util.*;
 import java.util.function.*;
 import java.util.stream.*;
@@ -101,6 +106,7 @@ import jakarta.servlet.http.*;
  *             </ul>
  *             <li>Methods for accessing HTTP parts:
  *             <ul class='javatreec'>
+ *                     <li class='jm'>{@link 
RestRequest#checkPreconditions(RestResponse) checkPreconditions(RestResponse)}
  *                     <li class='jm'>{@link 
RestRequest#containsFormParam(String) containsFormParam(String)}
  *                     <li class='jm'>{@link 
RestRequest#containsHeader(String) containsHeader(String)}
  *                     <li class='jm'>{@link 
RestRequest#containsQueryParam(String) containsQueryParam(String)}
@@ -388,6 +394,157 @@ public class RestRequest extends 
HttpServletRequestWrapper {
                return new FluentRequestLineAssertion<>(getRequestLine(), this);
        }
 
+       /**
+        * Evaluates the conditional-request headers on this request against 
the response's currently-set
+        * <c>ETag</c> and <c>Last-Modified</c> values.
+        *
+        * <p>
+        * Implements the precondition-evaluation order defined by
+        * <a class='doclink' 
href='https://www.rfc-editor.org/rfc/rfc7232#section-6'>RFC 7232 §6</a>:
+        * <ol>
+        *      <li><c>If-Match</c> — strong comparison; returns <c>412 
Precondition Failed</c> on mismatch.
+        *      <li><c>If-Unmodified-Since</c> — only consulted when 
<c>If-Match</c> is absent;
+        *              returns <c>412 Precondition Failed</c> when the 
response's <c>Last-Modified</c> is
+        *              strictly after the supplied date.
+        *      <li><c>If-None-Match</c> — weak comparison; returns <c>304 Not 
Modified</c> on match
+        *              (for safe methods, per RFC 7232 §6).
+        *      <li><c>If-Modified-Since</c> — only consulted when 
<c>If-None-Match</c> is absent;
+        *              returns <c>304 Not Modified</c> when the response's 
<c>Last-Modified</c> is at-or-before
+        *              the supplied date.
+        * </ol>
+        *
+        * <h5 class='section'>Usage:</h5>
+        *
+        * <p>
+        * Set the response's <c>ETag</c> and/or <c>Last-Modified</c> 
<b>first</b>, then call this method.
+        * The check reads the headers currently on the response, so any value 
set after the call is not
+        * considered.
+        *
+        * <p class='bjava'>
+        *      <ja>@RestGet</ja>(<js>"/{id}"</js>)
+        *      <jk>public</jk> Order get(<ja>@Path</ja> <jk>long</jk> 
<jv>id</jv>, RestRequest <jv>req</jv>, RestResponse <jv>res</jv>) {
+        *              <jk>var</jk> <jv>order</jv> = 
<jv>repo</jv>.find(<jv>id</jv>);
+        *              <jv>res</jv>.eTag(<js>"\""</js> + 
<jv>order</jv>.version() + 
<js>"\""</js>).lastModified(<jv>order</jv>.updated());
+        *              
<jv>req</jv>.checkPreconditions(<jv>res</jv>).ifPresent(<jv>e</jv> -&gt; { 
<jk>throw</jk> <jv>e</jv>; });
+        *              <jk>return</jk> <jv>order</jv>;
+        *      }
+        * </p>
+        *
+        * <h5 class='section'>Notes:</h5><ul>
+        *      <li class='note'>Returns an {@link Optional} so the caller 
controls how to handle the failure
+        *              (throw inline, hand off to a logger, etc.). Both the 
returned <c>304</c> and <c>412</c>
+        *              failures are {@link BasicHttpException} instances, 
which are {@link RuntimeException}s,
+        *              so {@code ifPresent(e -&gt; { throw e; })} compiles 
cleanly.
+        *      <li class='note'><c>If-None-Match: *</c> matches any present 
<c>ETag</c>; <c>If-Match: *</c>
+        *              fails only when no <c>ETag</c> is set on the response.
+        *      <li class='note'>Per RFC 7232 §3.1, <c>If-Match</c> requires 
strong comparison: a weak
+        *              <c>ETag</c> on the response side never satisfies an 
<c>If-Match</c>.
+        *      <li class='note'>Per RFC 7232 §3.2, <c>If-None-Match</c> uses 
weak comparison: the weak/strong
+        *              flag is ignored when comparing tag values.
+        * </ul>
+        *
+        * <h5 class='section'>See Also:</h5><ul>
+        *      <li class='jm'>{@link RestResponse#eTag(String) 
RestResponse.eTag(String)}
+        *      <li class='jm'>{@link RestResponse#lastModified(Instant) 
RestResponse.lastModified(Instant)}
+        * </ul>
+        *
+        * @param response The response carrying the resource's current 
<c>ETag</c> and/or <c>Last-Modified</c>.
+        *      Must not be <jk>null</jk>.
+        * @return Empty if all preconditions pass; otherwise an {@link 
Optional} holding either a
+        *      <c>304 Not Modified</c> {@link BasicHttpException} (when the 
request's cache copy is still
+        *      fresh) or a {@link PreconditionFailed} (when an <c>If-Match</c> 
/ <c>If-Unmodified-Since</c>
+        *      guard failed).
+        * @throws IllegalArgumentException If {@code response} is 
<jk>null</jk>.
+        */
+       public Optional<BasicHttpException> checkPreconditions(RestResponse 
response) {
+               assertArgNotNull("response", response);
+               var resTagStr = response.getHeader(ETag.NAME);
+               var resTag = nn(resTagStr) ? EntityTag.of(resTagStr) : null;
+               var resLastModStr = response.getHeader(LastModified.NAME);
+               var resLastMod = nn(resLastModStr) ? 
parseHttpDate(resLastModStr) : null;
+
+               // RFC 7232 §6 step 1: If-Match (strong comparison; 412 on 
failure).
+               var ifMatch = getHeaderParam(IfMatch.NAME).orElse(null);
+               if (nn(ifMatch)) {
+                       if (! matchesAnyStrong(EntityTags.of(ifMatch), resTag))
+                               return 
Optional.of(preconditionFailed(IfMatch.NAME));
+               } else {
+                       // RFC 7232 §6 step 2: If-Unmodified-Since (only when 
If-Match absent).
+                       var ius = 
getHeaderParam(IfUnmodifiedSince.NAME).orElse(null);
+                       if (nn(ius)) {
+                               var iusDate = parseHttpDate(ius);
+                               if (nn(iusDate) && nn(resLastMod) && 
resLastMod.toInstant().isAfter(iusDate.toInstant()))
+                                       return 
Optional.of(preconditionFailed(IfUnmodifiedSince.NAME));
+                       }
+               }
+
+               // RFC 7232 §6 step 3: If-None-Match (weak comparison; 304 on 
match for safe methods).
+               var ifNoneMatch = getHeaderParam(IfNoneMatch.NAME).orElse(null);
+               if (nn(ifNoneMatch)) {
+                       if (matchesAnyWeak(EntityTags.of(ifNoneMatch), resTag))
+                               return Optional.of(notModified());
+               } else {
+                       // RFC 7232 §6 step 4: If-Modified-Since (only when 
If-None-Match absent; only for GET/HEAD).
+                       if (isSafeMethod()) {
+                               var ims = 
getHeaderParam(IfModifiedSince.NAME).orElse(null);
+                               if (nn(ims)) {
+                                       var imsDate = parseHttpDate(ims);
+                                       if (nn(imsDate) && nn(resLastMod) && ! 
resLastMod.toInstant().isAfter(imsDate.toInstant()))
+                                               return 
Optional.of(notModified());
+                               }
+                       }
+               }
+
+               return Optional.empty();
+       }
+
+       private boolean isSafeMethod() {
+               var m = getMethod();
+               return "GET".equalsIgnoreCase(m) || "HEAD".equalsIgnoreCase(m);
+       }
+
+       private static ZonedDateTime parseHttpDate(String value) {
+               try {
+                       return 
ZonedDateTime.from(RFC_1123_DATE_TIME.parse(value)).truncatedTo(ChronoUnit.SECONDS);
+               } catch (@SuppressWarnings("unused") DateTimeParseException e) {
+                       return null;
+               }
+       }
+
+       private static boolean matchesAnyStrong(EntityTags clientTags, 
EntityTag resTag) {
+               if (clientTags == null)
+                       return false;
+               for (var t : clientTags.toArray()) {
+                       if (t.isAny())
+                               return resTag != null;
+                       // Strong comparison: both sides must be strong AND 
opaque-tag values must be equal.
+                       if (! t.isWeak() && resTag != null && ! resTag.isWeak() 
&& eq(t.getEntityValue(), resTag.getEntityValue()))
+                               return true;
+               }
+               return false;
+       }
+
+       private static boolean matchesAnyWeak(EntityTags clientTags, EntityTag 
resTag) {
+               if (clientTags == null)
+                       return false;
+               for (var t : clientTags.toArray()) {
+                       if (t.isAny())
+                               return resTag != null;
+                       // Weak comparison: opaque-tag values must be equal; 
weak/strong flag is ignored.
+                       if (resTag != null && eq(t.getEntityValue(), 
resTag.getEntityValue()))
+                               return true;
+               }
+               return false;
+       }
+
+       private static BasicHttpException notModified() {
+               return new BasicHttpException(NotModified.STATUS_CODE, 
NotModified.REASON_PHRASE);
+       }
+
+       private static PreconditionFailed preconditionFailed(String headerName) 
{
+               return new PreconditionFailed("Precondition ''{0}'' failed.", 
headerName);
+       }
+
        /**
         * Returns <jk>true</jk> if this request contains the specified header.
         *
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
index b966c94b77..8e0c3db3ca 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
@@ -18,12 +18,17 @@ package org.apache.juneau.rest;
 
 import org.apache.juneau.commons.http.StringRanges;
 import org.apache.juneau.commons.http.MediaType;
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
 import static org.apache.juneau.commons.utils.StringUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 import static org.apache.juneau.commons.httppart.HttpPartType.*;
 
+import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
+import static java.time.temporal.ChronoUnit.SECONDS;
+
 import java.io.*;
 import java.nio.charset.*;
+import java.time.*;
 import java.util.*;
 
 import org.apache.juneau.*;
@@ -81,6 +86,12 @@ import jakarta.servlet.http.*;
  *                     <li class='jm'>{@link 
RestResponse#setMaxHeaderLength(int) setMaxHeaderLength(int)}
  *                     <li class='jm'>{@link RestResponse#setSafeHeaders() 
setSafeHeaders()}
  *                     <li class='jm'>{@link RestResponse#downloadAs(String) 
downloadAs(String)}
+ *                     <li class='jm'>{@link RestResponse#eTag(String) 
eTag(String)}
+ *                     <li class='jm'>{@link RestResponse#eTag(EntityTag) 
eTag(EntityTag)}
+ *                     <li class='jm'>{@link 
RestResponse#lastModified(Instant) lastModified(Instant)}
+ *                     <li class='jm'>{@link 
RestResponse#lastModified(ZonedDateTime) lastModified(ZonedDateTime)}
+ *                     <li class='jm'>{@link RestResponse#cacheControl(String) 
cacheControl(String)}
+ *                     <li class='jm'>{@link 
RestResponse#cacheControl(CacheControlBuilder) 
cacheControl(CacheControlBuilder)}
  *             </ul>
  *             <li>Methods for setting response bodies:
  *             <ul class='javatreec'>
@@ -119,6 +130,7 @@ import jakarta.servlet.http.*;
 public class RestResponse extends HttpServletResponseWrapper {
 
        private static final String HEADER_ContentType = "Content-Type";
+       private static final String ARG_VALUE = "value";
 
        private HttpServletResponse inner;
        private final RestRequest request;
@@ -645,6 +657,9 @@ public class RestResponse extends 
HttpServletResponseWrapper {
         *      negotiation on the next {@link #getSerializerMatch()} call.
         * @return This object.
         */
+       @SuppressWarnings({
+               "java:S2789" // Null invalidates lazy Optional cache so 
getSerializerMatch() recomputes next access.
+       })
        public RestResponse setSerializer(Serializer value) {
                serializer = value;
                serializerMatch = null;
@@ -741,6 +756,120 @@ public class RestResponse extends 
HttpServletResponseWrapper {
                return setHeader(ContentDisposition.attachment(filename));
        }
 
+       /**
+        * Sets the <c>ETag</c> response header to the supplied wire value.
+        *
+        * <p>
+        * The value is validated through {@link EntityTag#of(Object)} — strong 
tags must be quoted
+        * (<c>"v1"</c>), weak tags must be prefixed with <c>W/</c> 
(<c>W/"v1"</c>), and the wildcard
+        * <c>*</c> is accepted. The header is serialized in canonical form 
(re-quoted for strong tags,
+        * <c>W/</c>-prefixed for weak tags).
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <jv>res</jv>.eTag(<js>"\"v42\""</js>);  <jc>// ETag: "v42"</jc>
+        *      <jv>res</jv>.eTag(<js>"W/\"v42\""</js>);  <jc>// ETag: 
W/"v42"</jc>
+        * </p>
+        *
+        * @param value The entity-tag wire value. Must not be <jk>null</jk>; 
must match the
+        *      {@code entity-tag} grammar per RFC 7232 §2.3.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code value} is malformed.
+        * @see RestRequest#checkPreconditions(RestResponse)
+        */
+       public RestResponse eTag(String value) {
+               return eTag(EntityTag.of(value));
+       }
+
+       /**
+        * Sets the <c>ETag</c> response header to the canonical wire form of 
the supplied {@link EntityTag}.
+        *
+        * @param value The entity tag. Must not be <jk>null</jk>.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
+        */
+       public RestResponse eTag(EntityTag value) {
+               assertArgNotNull(ARG_VALUE, value);
+               return setHeader(ETag.of(value.toString()));
+       }
+
+       /**
+        * Sets the <c>Last-Modified</c> response header to the supplied 
instant formatted as an
+        * <a class='doclink' 
href='https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1'>RFC 7231 
IMF-fixdate</a>.
+        *
+        * <p>
+        * The instant is rendered in GMT regardless of the JVM's default 
timezone. The fractional-second
+        * portion is truncated, matching the per-spec one-second resolution of 
HTTP-date timestamps.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      
<jv>res</jv>.lastModified(Instant.<jsm>parse</jsm>(<js>"2026-05-22T00:00:00Z"</js>));
+        *      <jc>// Last-Modified: Fri, 22 May 2026 00:00:00 GMT</jc>
+        * </p>
+        *
+        * @param value The last-modified timestamp. Must not be <jk>null</jk>.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
+        * @see RestRequest#checkPreconditions(RestResponse)
+        */
+       public RestResponse lastModified(Instant value) {
+               assertArgNotNull(ARG_VALUE, value);
+               return lastModified(value.atZone(ZoneOffset.UTC));
+       }
+
+       /**
+        * Sets the <c>Last-Modified</c> response header to the supplied 
date-time formatted as an
+        * <a class='doclink' 
href='https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1'>RFC 7231 
IMF-fixdate</a>.
+        *
+        * <p>
+        * The supplied date-time is converted to GMT and truncated to whole 
seconds before formatting.
+        *
+        * @param value The last-modified timestamp. Must not be <jk>null</jk>.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
+        */
+       public RestResponse lastModified(ZonedDateTime value) {
+               assertArgNotNull(ARG_VALUE, value);
+               var z = 
value.withZoneSameInstant(ZoneOffset.UTC).truncatedTo(SECONDS);
+               return setHeader(LastModified.of(RFC_1123_DATE_TIME.format(z)));
+       }
+
+       /**
+        * Sets the <c>Cache-Control</c> response header to the supplied 
directive string.
+        *
+        * <p>
+        * The value is written verbatim — the caller is responsible for 
supplying a comma-separated
+        * directive list per <a class='doclink' 
href='https://www.rfc-editor.org/rfc/rfc9111#name-cache-control'>RFC
+        * 9111 §5.2</a>.
+        *
+        * @param value The directive list (e.g. <js>"public, 
max-age=3600"</js>). Must not be <jk>null</jk>.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
+        * @see #cacheControl(CacheControlBuilder)
+        */
+       public RestResponse cacheControl(String value) {
+               assertArgNotNull(ARG_VALUE, value);
+               return setHeader(CacheControl.of(value));
+       }
+
+       /**
+        * Sets the <c>Cache-Control</c> response header from a {@link 
CacheControlBuilder}.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      
<jv>res</jv>.cacheControl(CacheControlBuilder.<jsm>create</jsm>().publicCache().maxAge(3600));
+        *      <jc>// Cache-Control: public, max-age=3600</jc>
+        * </p>
+        *
+        * @param value The builder. Must not be <jk>null</jk>.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
+        */
+       public RestResponse cacheControl(CacheControlBuilder value) {
+               assertArgNotNull(ARG_VALUE, value);
+               return cacheControl(value.build());
+       }
+
        /**
         * Sets a header on the request.
         *
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/http/header/NamedHeaders_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/http/header/NamedHeaders_Test.java
index fc69938242..99d03ab572 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/http/header/NamedHeaders_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/http/header/NamedHeaders_Test.java
@@ -69,6 +69,8 @@ class NamedHeaders_Test extends TestBase {
                // Value types (not HttpHeaderBean subclasses).
                "EntityTag",
                "EntityTags",
+               // Fluent builders (produce a header value String, not an 
HttpHeaderBean).
+               "CacheControlBuilder",
                "package-info"
        );
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/RestEtag_RoundTrip_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestEtag_RoundTrip_Test.java
new file mode 100644
index 0000000000..28a3d300dc
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestEtag_RoundTrip_Test.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest;
+
+import java.time.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * End-to-end "round trip" tests for the ETag / conditional-GET helpers in a 
realistic handler:
+ *
+ * <ul>
+ *   <li>First request gets the resource with {@code 200 OK} plus {@code ETag} 
and {@code Last-Modified} headers.
+ *   <li>Second request re-sends the same {@code ETag} via {@code 
If-None-Match} and gets {@code 304 Not Modified}.
+ *   <li>An update bumps the version + timestamp; the next request gets {@code 
200 OK} with the new tag/date.
+ *   <li>A {@code PUT} with a stale {@code If-Match} is rejected with {@code 
412 Precondition Failed}.
+ * </ul>
+ */
+class RestEtag_RoundTrip_Test extends TestBase {
+
+       /** Trivial in-memory order with a version + updated-instant for 
ETag/Last-Modified. */
+       public static class Order {
+               public String id, payload;
+               public long version;
+               public Instant updated;
+               public Order() {}
+               public Order(String id, String payload, long version, Instant 
updated) {
+                       this.id = id; this.payload = payload; this.version = 
version; this.updated = updated;
+               }
+       }
+
+       @Rest(serializers=JsonSerializer.class, parsers=JsonParser.class)
+       public static class R {
+               private final ConcurrentMap<String,Order> repo = new 
ConcurrentHashMap<>();
+
+               public R() {
+                       repo.put("1", new Order("1", "v1-payload", 1L, 
Instant.parse("2026-05-22T00:00:00Z")));
+               }
+
+               @RestGet("/orders/{id}")
+               public Order get(@Path("id") String id, RestRequest req, 
RestResponse res) {
+                       var order = repo.get(id);
+                       res.eTag("\"" + order.version + 
"\"").lastModified(order.updated);
+                       req.checkPreconditions(res).ifPresent(e -> { throw e; 
});
+                       return order;
+               }
+
+               @RestPut("/orders/{id}")
+               public Order put(@Path("id") String id, @Content Order in, 
RestRequest req, RestResponse res) {
+                       var current = repo.get(id);
+                       res.eTag("\"" + current.version + 
"\"").lastModified(current.updated);
+                       req.checkPreconditions(res).ifPresent(e -> { throw e; 
});
+                       var next = new Order(id, in.payload, current.version + 
1L, Instant.parse("2026-05-23T00:00:00Z"));
+                       repo.put(id, next);
+                       res.eTag("\"" + next.version + 
"\"").lastModified(next.updated);
+                       return next;
+               }
+       }
+
+       @Test void a01_firstGetReturnsEtagAndLastModified() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/orders/1").run().assertStatus(200)
+                       .assertHeader("ETag").is("\"1\"")
+                       .assertHeader("Last-Modified").is("Fri, 22 May 2026 
00:00:00 GMT");
+       }
+
+       @Test void a02_secondGetWithIfNoneMatchReturns304() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/orders/1").header("If-None-Match", 
"\"1\"").run().assertStatus(304);
+       }
+
+       @Test void a03_getWithIfModifiedSinceEqualReturns304() throws Exception 
{
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/orders/1").header("If-Modified-Since", "Fri, 22 May 
2026 00:00:00 GMT").run().assertStatus(304);
+       }
+
+       @Test void a04_putWithStaleIfMatchReturns412() throws Exception {
+               var c = 
MockRestClient.create(R.class).ignoreErrors().json().build();
+               // Stale version: client thinks the order is at version 0 (its 
real version is 1).
+               c.put("/orders/1", new Order("1", "bad", 0L, 
Instant.parse("2026-01-01T00:00:00Z")))
+                       .header("If-Match", "\"0\"").run().assertStatus(412);
+       }
+
+       @Test void a05_putWithCorrectIfMatchSucceedsAndBumpsEtag() throws 
Exception {
+               var c = 
MockRestClient.create(R.class).ignoreErrors().json().build();
+               c.put("/orders/1", new Order("1", "updated", 1L, 
Instant.parse("2026-01-01T00:00:00Z")))
+                       .header("If-Match", "\"1\"").run().assertStatus(200)
+                       .assertHeader("ETag").is("\"2\"")
+                       .assertHeader("Last-Modified").is("Sat, 23 May 2026 
00:00:00 GMT");
+               // After the bump, the original tag no longer matches — the 
next GET with the old tag returns 200.
+               c.get("/orders/1").header("If-None-Match", 
"\"1\"").run().assertStatus(200);
+               // And the new tag matches → 304.
+               c.get("/orders/1").header("If-None-Match", 
"\"2\"").run().assertStatus(304);
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/RestRequest_CheckPreconditions_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestRequest_CheckPreconditions_Test.java
new file mode 100644
index 0000000000..152cc0a1fe
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestRequest_CheckPreconditions_Test.java
@@ -0,0 +1,332 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest;
+
+import java.time.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link RestRequest#checkPreconditions(RestResponse)} — RFC 7232 
conditional-request handling.
+ *
+ * <p>
+ * The test resource exposes endpoints that set well-known {@code ETag} and 
{@code Last-Modified} values on the
+ * response and then short-circuits on {@code checkPreconditions(res)}: if the 
returned {@code Optional} is non-empty
+ * the helper exception is thrown (yielding 304 / 412); otherwise the handler 
returns 200 with body {@code "OK"}.
+ */
+class RestRequest_CheckPreconditions_Test extends TestBase {
+
+       // Fixed reference values used across tests.
+       private static final String ETAG_STRONG = "\"v1\"";
+       private static final String ETAG_WEAK = "W/\"v1\"";
+       private static final String LM = "Fri, 22 May 2026 00:00:00 GMT";
+       private static final String LM_EARLIER = "Thu, 21 May 2026 00:00:00 
GMT";
+       private static final String LM_LATER = "Sat, 23 May 2026 00:00:00 GMT";
+
+       @Rest
+       public static class R {
+               @RestGet("/strong")
+               public String strong(RestRequest req, RestResponse res) {
+                       
res.eTag(ETAG_STRONG).lastModified(Instant.parse("2026-05-22T00:00:00Z"));
+                       req.checkPreconditions(res).ifPresent(e -> { throw e; 
});
+                       return "OK";
+               }
+
+               @RestGet("/weak")
+               public String weak(RestRequest req, RestResponse res) {
+                       
res.eTag(ETAG_WEAK).lastModified(Instant.parse("2026-05-22T00:00:00Z"));
+                       req.checkPreconditions(res).ifPresent(e -> { throw e; 
});
+                       return "OK";
+               }
+
+               @RestGet("/lmOnly")
+               public String lmOnly(RestRequest req, RestResponse res) {
+                       res.lastModified(Instant.parse("2026-05-22T00:00:00Z"));
+                       req.checkPreconditions(res).ifPresent(e -> { throw e; 
});
+                       return "OK";
+               }
+
+               @RestGet("/etagOnly")
+               public String etagOnly(RestRequest req, RestResponse res) {
+                       res.eTag(ETAG_STRONG);
+                       req.checkPreconditions(res).ifPresent(e -> { throw e; 
});
+                       return "OK";
+               }
+
+               @RestGet("/bare")
+               public String bare(RestRequest req, RestResponse res) {
+                       req.checkPreconditions(res).ifPresent(e -> { throw e; 
});
+                       return "OK";
+               }
+
+               // Non-safe method to verify If-Modified-Since is ignored.
+               @RestPost("/postStrong")
+               public String postStrong(RestRequest req, RestResponse res) {
+                       
res.eTag(ETAG_STRONG).lastModified(Instant.parse("2026-05-22T00:00:00Z"));
+                       req.checkPreconditions(res).ifPresent(e -> { throw e; 
});
+                       return "OK";
+               }
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // A: No conditional headers → 200 OK (baseline).
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void a01_noConditionalHeadersPasses() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong").run().assertStatus(200).assertContent("OK");
+       }
+
+       @Test void a02_bareNoEtagNoLmPasses() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/bare").run().assertStatus(200).assertContent("OK");
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // B: If-Match (strong comparison; 412 on failure).
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void b01_ifMatchExactStrongMatches() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong").header("If-Match", 
ETAG_STRONG).run().assertStatus(200);
+       }
+
+       @Test void b02_ifMatchMismatchYields412() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong").header("If-Match", 
"\"v2\"").run().assertStatus(412);
+       }
+
+       @Test void b03_ifMatchWildcardWithEtagPresentMatches() throws Exception 
{
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong").header("If-Match", 
"*").run().assertStatus(200);
+       }
+
+       @Test void b04_ifMatchWildcardWithNoEtagFails() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly").header("If-Match", 
"*").run().assertStatus(412);
+       }
+
+       @Test void b05_ifMatchAgainstWeakEtagFails() throws Exception {
+               // RFC 7232 §3.1: If-Match requires strong comparison; weak 
server ETag never matches.
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/weak").header("If-Match", 
ETAG_WEAK).run().assertStatus(412);
+               c.get("/weak").header("If-Match", 
"\"v1\"").run().assertStatus(412);
+       }
+
+       @Test void b06_ifMatchMultipleTagsAnyMatches() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong").header("If-Match", "\"v2\", \"v1\", 
\"v3\"").run().assertStatus(200);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // C: If-Unmodified-Since (only when If-Match absent; 412 if resource 
newer than supplied).
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void c01_ifUnmodifiedSinceLaterPasses() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly").header("If-Unmodified-Since", 
LM_LATER).run().assertStatus(200);
+       }
+
+       @Test void c02_ifUnmodifiedSinceEqualPasses() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly").header("If-Unmodified-Since", 
LM).run().assertStatus(200);
+       }
+
+       @Test void c03_ifUnmodifiedSinceEarlierFails() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly").header("If-Unmodified-Since", 
LM_EARLIER).run().assertStatus(412);
+       }
+
+       @Test void c04_ifUnmodifiedSinceIgnoredWhenIfMatchPresent() throws 
Exception {
+               // Per RFC 7232 §6, If-Unmodified-Since is ignored when 
If-Match is set. Even if If-Unmodified-Since
+               // would fail in isolation, the If-Match must win (and here, 
succeed).
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong")
+                       .header("If-Match", ETAG_STRONG)
+                       .header("If-Unmodified-Since", LM_EARLIER)  // would 
fail standalone
+                       .run().assertStatus(200);
+       }
+
+       @Test void c05_ifUnmodifiedSinceWithoutResourceLastModifiedIgnored() 
throws Exception {
+               // No Last-Modified set on the response, so the date comparison 
is skipped.
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/etagOnly").header("If-Unmodified-Since", 
LM_EARLIER).run().assertStatus(200);
+       }
+
+       @Test void c06_ifUnmodifiedSinceMalformedIgnored() throws Exception {
+               // Unparseable date is treated as not-set per Postel's law and 
the RFC ignores garbled dates.
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly").header("If-Unmodified-Since", 
"not-a-date").run().assertStatus(200);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // D: If-None-Match (weak comparison; 304 on match).
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void d01_ifNoneMatchExactMatches304() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong").header("If-None-Match", 
ETAG_STRONG).run().assertStatus(304);
+       }
+
+       @Test void d02_ifNoneMatchMismatchPasses200() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong").header("If-None-Match", 
"\"v2\"").run().assertStatus(200);
+       }
+
+       @Test void d03_ifNoneMatchWildcardWithEtag304() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong").header("If-None-Match", 
"*").run().assertStatus(304);
+       }
+
+       @Test void d04_ifNoneMatchWildcardWithoutEtagPasses() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly").header("If-None-Match", 
"*").run().assertStatus(200);
+       }
+
+       @Test void d05_ifNoneMatchWeakStrongCompareSucceeds() throws Exception {
+               // RFC 7232 §3.2: If-None-Match uses weak comparison; weak vs 
strong both count as a match.
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong").header("If-None-Match", 
ETAG_WEAK).run().assertStatus(304);
+               c.get("/weak").header("If-None-Match", 
ETAG_STRONG).run().assertStatus(304);
+               c.get("/weak").header("If-None-Match", 
ETAG_WEAK).run().assertStatus(304);
+       }
+
+       @Test void d06_ifNoneMatchMultipleAnyMatches304() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong").header("If-None-Match", "\"v2\", \"v1\", 
\"v3\"").run().assertStatus(304);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // E: If-Modified-Since (only when If-None-Match absent; only on 
GET/HEAD; 304 when not modified).
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void e01_ifModifiedSinceEqualReturns304() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly").header("If-Modified-Since", 
LM).run().assertStatus(304);
+       }
+
+       @Test void e02_ifModifiedSinceLaterReturns304() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly").header("If-Modified-Since", 
LM_LATER).run().assertStatus(304);
+       }
+
+       @Test void e03_ifModifiedSinceEarlierPasses200() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly").header("If-Modified-Since", 
LM_EARLIER).run().assertStatus(200);
+       }
+
+       @Test void e04_ifModifiedSinceIgnoredWhenIfNoneMatchPresent() throws 
Exception {
+               // Per RFC 7232 §6, If-Modified-Since is ignored when 
If-None-Match is set.
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong")
+                       .header("If-None-Match", "\"v2\"")          // mismatch 
→ would pass standalone
+                       .header("If-Modified-Since", LM)             // would 
304 standalone
+                       .run().assertStatus(200);                    // 
If-None-Match wins, no 304
+       }
+
+       @Test void e05_ifModifiedSinceIgnoredOnPost() throws Exception {
+               // Only GET / HEAD honor If-Modified-Since per RFC 7232 §6.
+               var c = MockRestClient.buildLax(R.class);
+               c.post("/postStrong", "x").header("If-Modified-Since", 
LM).run().assertStatus(200);
+       }
+
+       @Test void e06_ifModifiedSinceWithoutResourceLastModifiedPasses() 
throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/etagOnly").header("If-Modified-Since", 
LM).run().assertStatus(200);
+       }
+
+       @Test void e07_ifModifiedSinceMalformedIgnored() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly").header("If-Modified-Since", 
"not-a-date").run().assertStatus(200);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // F: Combinations of all four conditional headers — 16-cell precedence 
matrix.
+       //
+       // Layout: (M=If-Match, U=If-Unmodified-Since, N=If-None-Match, 
S=If-Modified-Since)
+       //   - present-with-pass and present-with-fail are exercised in B/C/D/E.
+       //   - here we exercise the cross-products that depend on RFC 7232 §6 
ordering.
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void f01_allFourPass_returns200() throws Exception {
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong")
+                       .header("If-Match", ETAG_STRONG)
+                       .header("If-Unmodified-Since", LM_LATER)
+                       .header("If-None-Match", "\"v2\"")
+                       .header("If-Modified-Since", LM_EARLIER)
+                       .run().assertStatus(200);
+       }
+
+       @Test void f02_ifMatchFails_returns412_evenWithNoneMatchPass() throws 
Exception {
+               // If-Match short-circuits at step 1; If-None-Match steps are 
not consulted.
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong")
+                       .header("If-Match", "\"v2\"")
+                       .header("If-None-Match", ETAG_STRONG)  // would also 
304 standalone
+                       .run().assertStatus(412);
+       }
+
+       @Test void f03_ifMatchPasses_thenIfNoneMatchTriggers304() throws 
Exception {
+               // If-Match passes (step 1), If-Unmodified-Since skipped (step 
2), If-None-Match triggers (step 3).
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/strong")
+                       .header("If-Match", ETAG_STRONG)
+                       .header("If-None-Match", ETAG_STRONG)
+                       .run().assertStatus(304);
+       }
+
+       @Test void f04_unmodifiedSincePassed_thenModifiedSinceTriggers304() 
throws Exception {
+               // If-Match absent → If-Unmodified-Since checked (passes). 
If-None-Match absent → If-Modified-Since
+               // triggers 304.
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly")
+                       .header("If-Unmodified-Since", LM_LATER)
+                       .header("If-Modified-Since", LM)
+                       .run().assertStatus(304);
+       }
+
+       @Test void f05_unmodifiedSinceFails_412_beforeModifiedSinceEvaluated() 
throws Exception {
+               // If-Unmodified-Since fails at step 2; If-Modified-Since never 
consulted.
+               var c = MockRestClient.buildLax(R.class);
+               c.get("/lmOnly")
+                       .header("If-Unmodified-Since", LM_EARLIER)
+                       .header("If-Modified-Since", LM_LATER)  // would 304 
standalone
+                       .run().assertStatus(412);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // G: Null-argument guard.
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Rest
+       public static class G {
+               @RestGet("/nullResponse")
+               public String nullResponse(RestRequest req) {
+                       req.checkPreconditions(null);
+                       return "OK";
+               }
+       }
+
+       @Test void g01_nullResponseRejected() throws Exception {
+               var c = MockRestClient.buildLax(G.class);
+               c.get("/nullResponse").run().assertStatus(500);
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/RestResponse_EtagHelpers_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestResponse_EtagHelpers_Test.java
new file mode 100644
index 0000000000..3d19684f00
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestResponse_EtagHelpers_Test.java
@@ -0,0 +1,364 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for the conditional-GET response helpers on {@link RestResponse}:
+ * {@code eTag(...)}, {@code lastModified(...)}, and {@code cacheControl(...)}.
+ */
+class RestResponse_EtagHelpers_Test extends TestBase {
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // A: eTag(...) — strong vs weak vs wildcard wire-format
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Rest
+       public static class A {
+               @RestGet("/strongString")
+               public String strongString(RestResponse res) {
+                       res.eTag("\"v42\"");
+                       return "ok";
+               }
+
+               @RestGet("/weakString")
+               public String weakString(RestResponse res) {
+                       res.eTag("W/\"v42\"");
+                       return "ok";
+               }
+
+               @RestGet("/wildcardString")
+               public String wildcardString(RestResponse res) {
+                       res.eTag("*");
+                       return "ok";
+               }
+
+               @RestGet("/strongTyped")
+               public String strongTyped(RestResponse res) {
+                       res.eTag(EntityTag.of("\"v42\""));
+                       return "ok";
+               }
+
+               @RestGet("/weakTyped")
+               public String weakTyped(RestResponse res) {
+                       res.eTag(EntityTag.of("W/\"v42\""));
+                       return "ok";
+               }
+       }
+
+       @Test void a01_strongStringEmitsQuoted() throws Exception {
+               var a = MockRestClient.buildLax(A.class);
+               
a.get("/strongString").run().assertStatus(200).assertHeader("ETag").is("\"v42\"");
+       }
+
+       @Test void a02_weakStringEmitsWeakPrefix() throws Exception {
+               var a = MockRestClient.buildLax(A.class);
+               
a.get("/weakString").run().assertStatus(200).assertHeader("ETag").is("W/\"v42\"");
+       }
+
+       @Test void a03_wildcardStringEmitsStar() throws Exception {
+               var a = MockRestClient.buildLax(A.class);
+               
a.get("/wildcardString").run().assertStatus(200).assertHeader("ETag").is("*");
+       }
+
+       @Test void a04_strongTypedEmitsQuoted() throws Exception {
+               var a = MockRestClient.buildLax(A.class);
+               
a.get("/strongTyped").run().assertStatus(200).assertHeader("ETag").is("\"v42\"");
+       }
+
+       @Test void a05_weakTypedEmitsWeakPrefix() throws Exception {
+               var a = MockRestClient.buildLax(A.class);
+               
a.get("/weakTyped").run().assertStatus(200).assertHeader("ETag").is("W/\"v42\"");
+       }
+
+       @Test void a06_malformedStringRejected() {
+               // Direct call into EntityTag.of validates - bare unquoted tags 
throw.
+               assertThrows(IllegalArgumentException.class, () -> 
EntityTag.of("v42"));
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // B: lastModified(...) — IMF-fixdate formatting, instant vs zoned, UTC 
conversion, truncation
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Rest
+       public static class B {
+               @RestGet("/fromInstant")
+               public String fromInstant(RestResponse res) {
+                       res.lastModified(Instant.parse("2026-05-22T00:00:00Z"));
+                       return "ok";
+               }
+
+               @RestGet("/fromZonedUtc")
+               public String fromZonedUtc(RestResponse res) {
+                       
res.lastModified(ZonedDateTime.parse("2026-05-22T00:00:00Z"));
+                       return "ok";
+               }
+
+               @RestGet("/fromZonedOffset")
+               public String fromZonedOffset(RestResponse res) {
+                       // 02:30 +02:30 = 00:00 UTC ⇒ same wire result as 
fromZonedUtc.
+                       
res.lastModified(ZonedDateTime.parse("2026-05-22T02:30:00+02:30"));
+                       return "ok";
+               }
+
+               @RestGet("/truncatesFraction")
+               public String truncatesFraction(RestResponse res) {
+                       
res.lastModified(Instant.parse("2026-05-22T00:00:00.987654321Z"));
+                       return "ok";
+               }
+       }
+
+       @Test void b01_instantFormattedAsImfFixdate() throws Exception {
+               var a = MockRestClient.buildLax(B.class);
+               a.get("/fromInstant").run().assertStatus(200)
+                       .assertHeader("Last-Modified").is("Fri, 22 May 2026 
00:00:00 GMT");
+       }
+
+       @Test void b02_zonedUtcFormattedAsImfFixdate() throws Exception {
+               var a = MockRestClient.buildLax(B.class);
+               a.get("/fromZonedUtc").run().assertStatus(200)
+                       .assertHeader("Last-Modified").is("Fri, 22 May 2026 
00:00:00 GMT");
+       }
+
+       @Test void b03_zonedOffsetConvertedToUtc() throws Exception {
+               var a = MockRestClient.buildLax(B.class);
+               a.get("/fromZonedOffset").run().assertStatus(200)
+                       .assertHeader("Last-Modified").is("Fri, 22 May 2026 
00:00:00 GMT");
+       }
+
+       @Test void b04_fractionalSecondsTruncated() throws Exception {
+               var a = MockRestClient.buildLax(B.class);
+               a.get("/truncatesFraction").run().assertStatus(200)
+                       .assertHeader("Last-Modified").is("Fri, 22 May 2026 
00:00:00 GMT");
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // C: cacheControl(...) — string vs builder
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Rest
+       public static class C {
+               @RestGet("/fromString")
+               public String fromString(RestResponse res) {
+                       res.cacheControl("public, max-age=3600");
+                       return "ok";
+               }
+
+               @RestGet("/fromBuilder")
+               public String fromBuilder(RestResponse res) {
+                       
res.cacheControl(CacheControlBuilder.create().publicCache().maxAge(3600).mustRevalidate());
+                       return "ok";
+               }
+
+               @RestGet("/privateNoStore")
+               public String privateNoStore(RestResponse res) {
+                       
res.cacheControl(CacheControlBuilder.create().privateCache().noStore());
+                       return "ok";
+               }
+
+               @RestGet("/immutable")
+               public String immutable(RestResponse res) {
+                       
res.cacheControl(CacheControlBuilder.create().publicCache().maxAge(31536000L).immutable());
+                       return "ok";
+               }
+       }
+
+       @Test void c01_stringFormVerbatim() throws Exception {
+               var a = MockRestClient.buildLax(C.class);
+               
a.get("/fromString").run().assertStatus(200).assertHeader("Cache-Control").is("public,
 max-age=3600");
+       }
+
+       @Test void c02_builderFormJoinedCommaSeparated() throws Exception {
+               var a = MockRestClient.buildLax(C.class);
+               // Builder emits a stable order: cacheability → boolean 
directives → numeric directives → extensions.
+               a.get("/fromBuilder").run().assertStatus(200)
+                       .assertHeader("Cache-Control").is("public, 
must-revalidate, max-age=3600");
+       }
+
+       @Test void c03_builderPrivateNoStore() throws Exception {
+               var a = MockRestClient.buildLax(C.class);
+               a.get("/privateNoStore").run().assertStatus(200)
+                       .assertHeader("Cache-Control").is("private, no-store");
+       }
+
+       @Test void c04_builderImmutableMaxAge() throws Exception {
+               var a = MockRestClient.buildLax(C.class);
+               // Booleans before numbers in the stable emission order.
+               a.get("/immutable").run().assertStatus(200)
+                       .assertHeader("Cache-Control").is("public, immutable, 
max-age=31536000");
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // D: CacheControlBuilder unit checks — directives, ordering, error 
paths
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void d01_emptyBuilderEmitsEmptyString() {
+               assertEquals("", CacheControlBuilder.create().build());
+       }
+
+       @Test void d02_publicOverridesPrivate() {
+               var s = 
CacheControlBuilder.create().privateCache().publicCache().build();
+               assertEquals("public", s);
+       }
+
+       @Test void d03_privateOverridesPublic() {
+               var s = 
CacheControlBuilder.create().publicCache().privateCache().build();
+               assertEquals("private", s);
+       }
+
+       @Test void d04_allBooleanDirectives() {
+               // Stable emission order: public/private, then boolean 
directives in source order.
+               var s = CacheControlBuilder.create()
+                       .publicCache()
+                       .noCache().noStore().noTransform()
+                       .mustRevalidate().proxyRevalidate()
+                       .immutable()
+                       .build();
+               assertEquals("public, no-cache, no-store, no-transform, 
must-revalidate, proxy-revalidate, immutable", s);
+       }
+
+       @Test void d05_maxAgeAndSMaxAge() {
+               var s = CacheControlBuilder.create()
+                       .maxAge(60)
+                       .sMaxAge(120)
+                       .build();
+               assertEquals("max-age=60, s-maxage=120", s);
+       }
+
+       @Test void d06_maxAgeFromDuration() {
+               var s = CacheControlBuilder.create()
+                       .maxAge(Duration.ofMinutes(10))
+                       .sMaxAge(Duration.ofMinutes(15))
+                       .build();
+               assertEquals("max-age=600, s-maxage=900", s);
+       }
+
+       @Test void d07_staleWhileRevalidateAndStaleIfError() {
+               var s = CacheControlBuilder.create()
+                       .staleWhileRevalidate(30)
+                       .staleIfError(60)
+                       .build();
+               assertEquals("stale-while-revalidate=30, stale-if-error=60", s);
+       }
+
+       @Test void d08_extensionAppendedAtEnd() {
+               var s = CacheControlBuilder.create()
+                       .publicCache().maxAge(3600)
+                       .extension("community=\"UCI\"")
+                       .build();
+               assertEquals("public, max-age=3600, community=\"UCI\"", s);
+       }
+
+       @Test void d09_extensionRejectsBlank() {
+               assertThrows(IllegalArgumentException.class, () -> 
CacheControlBuilder.create().extension(""));
+               assertThrows(IllegalArgumentException.class, () -> 
CacheControlBuilder.create().extension("   "));
+       }
+
+       @Test void d10_extensionRejectsNull() {
+               assertThrows(IllegalArgumentException.class, () -> 
CacheControlBuilder.create().extension(null));
+       }
+
+       @Test void d11_negativeMaxAgeRejected() {
+               assertThrows(IllegalArgumentException.class, () -> 
CacheControlBuilder.create().maxAge(-1));
+               assertThrows(IllegalArgumentException.class, () -> 
CacheControlBuilder.create().sMaxAge(-1));
+               assertThrows(IllegalArgumentException.class, () -> 
CacheControlBuilder.create().staleWhileRevalidate(-1));
+               assertThrows(IllegalArgumentException.class, () -> 
CacheControlBuilder.create().staleIfError(-1));
+       }
+
+       @Test void d12_nullDurationRejected() {
+               assertThrows(IllegalArgumentException.class, () -> 
CacheControlBuilder.create().maxAge((Duration)null));
+               assertThrows(IllegalArgumentException.class, () -> 
CacheControlBuilder.create().sMaxAge((Duration)null));
+       }
+
+       @Test void d13_toHeaderReturnsCacheControlBean() {
+               var h = 
CacheControlBuilder.create().publicCache().maxAge(60).toHeader();
+               assertNotNull(h);
+               assertEquals("Cache-Control", h.getName());
+               assertEquals("public, max-age=60", h.getValue());
+       }
+
+       @Test void d14_toStringMatchesBuild() {
+               var b = CacheControlBuilder.create().publicCache().maxAge(60);
+               assertEquals(b.build(), b.toString());
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // E: Null-argument guards on RestResponse helpers (cover the 
assertArgNotNull branches).
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Rest
+       public static class E {
+               @RestGet("/etagNullString") public String 
etagNullString(RestResponse res) {
+                       res.eTag((String)null); return "ok";
+               }
+               @RestGet("/etagNullTyped") public String 
etagNullTyped(RestResponse res) {
+                       res.eTag((EntityTag)null); return "ok";
+               }
+               @RestGet("/lmNullInstant") public String 
lmNullInstant(RestResponse res) {
+                       res.lastModified((Instant)null); return "ok";
+               }
+               @RestGet("/lmNullZdt") public String lmNullZdt(RestResponse 
res) {
+                       res.lastModified((ZonedDateTime)null); return "ok";
+               }
+               @RestGet("/ccNullString") public String 
ccNullString(RestResponse res) {
+                       res.cacheControl((String)null); return "ok";
+               }
+               @RestGet("/ccNullBuilder") public String 
ccNullBuilder(RestResponse res) {
+                       res.cacheControl((CacheControlBuilder)null); return 
"ok";
+               }
+       }
+
+       @Test void e01_nullEtagStringYields500() throws Exception {
+               // EntityTag.of(null) returns null, then assertArgNotNull 
triggers IllegalArgumentException → 500.
+               var a = MockRestClient.buildLax(E.class);
+               a.get("/etagNullString").run().assertStatus(500);
+       }
+
+       @Test void e02_nullEtagTypedYields500() throws Exception {
+               var a = MockRestClient.buildLax(E.class);
+               a.get("/etagNullTyped").run().assertStatus(500);
+       }
+
+       @Test void e03_nullInstantYields500() throws Exception {
+               var a = MockRestClient.buildLax(E.class);
+               a.get("/lmNullInstant").run().assertStatus(500);
+       }
+
+       @Test void e04_nullZdtYields500() throws Exception {
+               var a = MockRestClient.buildLax(E.class);
+               a.get("/lmNullZdt").run().assertStatus(500);
+       }
+
+       @Test void e05_nullCacheControlStringYields500() throws Exception {
+               var a = MockRestClient.buildLax(E.class);
+               a.get("/ccNullString").run().assertStatus(500);
+       }
+
+       @Test void e06_nullCacheControlBuilderYields500() throws Exception {
+               var a = MockRestClient.buildLax(E.class);
+               a.get("/ccNullBuilder").run().assertStatus(500);
+       }
+}
diff --git a/todo/TODO-64-etag-conditional-get-helpers.md 
b/todo/FINISHED-64-etag-conditional-get-helpers.md
similarity index 83%
rename from todo/TODO-64-etag-conditional-get-helpers.md
rename to todo/FINISHED-64-etag-conditional-get-helpers.md
index 2926a99782..df3dcc4a79 100644
--- a/todo/TODO-64-etag-conditional-get-helpers.md
+++ b/todo/FINISHED-64-etag-conditional-get-helpers.md
@@ -1,4 +1,6 @@
-# TODO-64: Conditional-GET / ETag / `If-Modified-Since` helpers on 
`RestResponse`
+# FINISHED-64: Conditional-GET / ETag / `If-Modified-Since` helpers on 
`RestResponse`
+
+**Completed 2026-05-22.** Shipped the RFC 7232 conditional-request layer 
end-to-end: six fluent setters on `RestResponse` (`eTag(String)` / 
`eTag(EntityTag)`, `lastModified(Instant)` / `lastModified(ZonedDateTime)`, 
`cacheControl(String)` / `cacheControl(CacheControlBuilder)`), a new typed 
`CacheControlBuilder` in `juneau-rest-common` covering every standard directive 
(`public` / `private`, `no-cache` / `no-store` / `no-transform`, 
`must-revalidate` / `proxy-revalidate`, `immutable`, `ma [...]
 
 Source: split out of TODO-18 brainstorm on 2026-05-22.
 
diff --git a/todo/TODO.md b/todo/TODO.md
index dbfb042a21..3ba3250d0d 100644
--- a/todo/TODO.md
+++ b/todo/TODO.md
@@ -6,8 +6,6 @@
 
 - [TODO-37] - Agent instruction consolidation.
 
-- [TODO-64] Conditional-GET / ETag / `If-Modified-Since` helpers on 
`RestResponse`. See `todo/TODO-64-etag-conditional-get-helpers.md`.
-
 - [TODO-65] Health / readiness / liveness probe endpoints + `HealthIndicator` 
SPI. See `todo/TODO-65-health-readiness-liveness-probes.md`.
 
 - [TODO-66] Rate-limit guard + request-id propagation filter. See 
`todo/TODO-66-rate-limit-and-request-id.md`.

Reply via email to