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

remm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 990f7e65ff Improve HTTP If headers processing according to RFC 9110
990f7e65ff is described below

commit 990f7e65fff23755bdda505225b77843a189107f
Author: remm <r...@apache.org>
AuthorDate: Wed Dec 11 10:56:06 2024 +0100

    Improve HTTP If headers processing according to RFC 9110
    
    PR#796 by Chenjp.
    Also includes better test cases.
---
 .../apache/catalina/servlets/DefaultServlet.java   | 376 +++++++++---
 .../TestDefaultServletRfc9110Section13.java        | 672 +++++++++++++++++++++
 ...efaultServletRfc9110Section13Parameterized.java | 433 +++++++++++++
 webapps/docs/changelog.xml                         |   4 +
 4 files changed, 1387 insertions(+), 98 deletions(-)

diff --git a/java/org/apache/catalina/servlets/DefaultServlet.java 
b/java/org/apache/catalina/servlets/DefaultServlet.java
index e5dff13949..8eb1ba8524 100644
--- a/java/org/apache/catalina/servlets/DefaultServlet.java
+++ b/java/org/apache/catalina/servlets/DefaultServlet.java
@@ -726,10 +726,72 @@ public class DefaultServlet extends HttpServlet {
      */
     protected boolean checkIfHeaders(HttpServletRequest request, 
HttpServletResponse response, WebResource resource)
             throws IOException {
+        String ifNoneMatchHeader = request.getHeader("If-None-Match");
 
-        return checkIfMatch(request, response, resource) && 
checkIfModifiedSince(request, response, resource) &&
-                checkIfNoneMatch(request, response, resource) && 
checkIfUnmodifiedSince(request, response, resource);
-
+        // RFC9110 #13.3.2 defines preconditions evaluation order
+        int next = 1;
+        while (true) {
+            switch (next) {
+                case 1:
+                    if (request.getHeader("If-Match") != null) {
+                        if (checkIfMatch(request, response, resource)) {
+                            next = 3;
+                        } else {
+                            return false;
+                        }
+                    } else {
+                        next = 2;
+                    }
+                    break;
+                case 2:
+                    if (request.getHeader("If-Unmodified-Since") != null) {
+                        if (checkIfUnmodifiedSince(request, response, 
resource)) {
+                            next = 3;
+                        } else {
+                            return false;
+                        }
+                    } else {
+                        next = 3;
+                    }
+                    break;
+                case 3:
+                    if (ifNoneMatchHeader != null) {
+                        if (checkIfNoneMatch(request, response, resource)) {
+                            next = 5;
+                        } else {
+                            return false;
+                        }
+                    } else {
+                        next = 4;
+                    }
+                    break;
+                case 4:
+                    if (("GET".equals(request.getMethod()) || 
"HEAD".equals(request.getMethod())) &&
+                            ifNoneMatchHeader == null && 
request.getHeader("If-Modified-Since") != null) {
+                        if (checkIfModifiedSince(request, response, resource)) 
{
+                            next = 5;
+                        } else {
+                            return false;
+                        }
+                    } else {
+                        next = 5;
+                    }
+                    break;
+                case 5:
+                    if ("GET".equals(request.getMethod()) && 
request.getHeader("If-Range") != null
+                        && request.getHeader("Range") != null) {
+                        if (checkIfRange(request, response, resource) && 
determineRangeRequestsApplicable(resource)) {
+                            // Partial content, precondition passed
+                            return true;
+                        } else {
+                            // ignore the Range header field
+                            return true;
+                        }
+                    } else {
+                        return true;
+                    }
+            }
+        }
     }
 
 
@@ -825,15 +887,6 @@ public class DefaultServlet extends HttpServlet {
         }
 
         boolean included = false;
-        // Check if the conditions specified in the optional If headers are
-        // satisfied.
-        if (resource.isFile()) {
-            // Checking If headers
-            included = 
(request.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
-            if (!included && !isError && !checkIfHeaders(request, response, 
resource)) {
-                return;
-            }
-        }
 
         // Find content type.
         String contentType = resource.getMimeType();
@@ -847,11 +900,21 @@ public class DefaultServlet extends HttpServlet {
         // be needed later
         String eTag = null;
         String lastModifiedHttp = null;
+
         if (resource.isFile() && !isError) {
             eTag = generateETag(resource);
             lastModifiedHttp = resource.getLastModifiedHttp();
         }
 
+        // Check if the conditions specified in the optional If headers are
+        // satisfied.
+        if (resource.isFile()) {
+            // Checking If headers
+            included = 
(request.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
+            if (!included && !isError && !checkIfHeaders(request, response, 
resource)) {
+                return;
+            }
+        }
 
         // Serve a precompressed version of the file if present
         boolean usingPrecompressedVersion = false;
@@ -1445,41 +1508,24 @@ public class DefaultServlet extends HttpServlet {
     protected Ranges parseRange(HttpServletRequest request, 
HttpServletResponse response, WebResource resource)
             throws IOException {
 
-        if (!"GET".equals(request.getMethod())) {
+        // Retrieving the range header (if any is specified)
+        String rangeHeader = request.getHeader("Range");
+
+        if (rangeHeader == null) {
+            // No Range header is the same as ignoring any Range header
+            return FULL;
+        }
+
+        if (!"GET".equals(request.getMethod()) || 
!determineRangeRequestsApplicable(resource)) {
             // RFC 9110 - Section 14.2: GET is the only method for which range 
handling is defined.
             // Otherwise MUST ignore a Range header field
             return FULL;
         }
 
-        // Checking If-Range
-        String headerValue = request.getHeader("If-Range");
-
-        if (headerValue != null) {
-
-            long headerValueTime = (-1L);
-            try {
-                headerValueTime = request.getDateHeader("If-Range");
-            } catch (IllegalArgumentException e) {
-                // Ignore
-            }
-
-            String eTag = generateETag(resource);
-            long lastModified = resource.getLastModified();
-
-            if (headerValueTime == (-1L)) {
-                // If the ETag the client gave does not match the entity
-                // etag, then the entire entity is returned.
-                if (!eTag.equals(headerValue.trim())) {
-                    return FULL;
-                }
-            } else {
-                // If the timestamp of the entity the client got differs from
-                // the last modification date of the entity, the entire entity
-                // is returned.
-                if (Math.abs(lastModified - headerValueTime) > 1000) {
-                    return FULL;
-                }
-            }
+        // Although If-Range evaluation was performed previously, the result 
were not propagated.
+        // Hence we have to evaluate If-Range again.
+        if (!checkIfRange(request, response, resource)) {
+            return FULL;
         }
 
         long fileLength = resource.getContentLength();
@@ -1490,13 +1536,6 @@ public class DefaultServlet extends HttpServlet {
             return FULL;
         }
 
-        // Retrieving the range header (if any is specified)
-        String rangeHeader = request.getHeader("Range");
-
-        if (rangeHeader == null) {
-            // No Range header is the same as ignoring any Range header
-            return FULL;
-        }
 
         Ranges ranges = Ranges.parse(new StringReader(rangeHeader));
 
@@ -2092,36 +2131,49 @@ public class DefaultServlet extends HttpServlet {
     protected boolean checkIfMatch(HttpServletRequest request, 
HttpServletResponse response, WebResource resource)
             throws IOException {
 
-        String headerValue = request.getHeader("If-Match");
-        if (headerValue != null) {
-
-            boolean conditionSatisfied;
+        String resourceETag = generateETag(resource);
+        if (resourceETag == null) {
+            // if a current representation for the target resource is not 
present
+            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+            return false;
+        }
 
-            if (!headerValue.equals("*")) {
-                String resourceETag = generateETag(resource);
-                if (resourceETag == null) {
-                    conditionSatisfied = false;
-                } else {
-                    // RFC 7232 requires strong comparison for If-Match headers
-                    Boolean matched = EntityTag.compareEntityTag(new 
StringReader(headerValue), false, resourceETag);
-                    if (matched == null) {
-                        if (debug > 10) {
-                            log("DefaultServlet.checkIfMatch:  Invalid header 
value [" + headerValue + "]");
-                        }
-                        response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-                        return false;
+        boolean conditionSatisfied = false;
+        Enumeration<String> headerValues = request.getHeaders("If-Match");
+        if (!headerValues.hasMoreElements()) {
+            return true;
+        }
+        boolean hasAsteriskValue = false;// check existence of special header 
value '*'
+        while (headerValues.hasMoreElements() && !conditionSatisfied) {
+            String headerValue = headerValues.nextElement();
+            if ("*".equals(headerValue)) {
+                hasAsteriskValue = true;
+                conditionSatisfied = true;
+            } else {
+                // RFC 7232 requires strong comparison for If-Match headers
+                Boolean matched = EntityTag.compareEntityTag(new 
StringReader(headerValue), false, resourceETag);
+                if (matched == null) {
+                    if (debug > 10) {
+                        log("DefaultServlet.checkIfMatch:  Invalid header 
value [" + headerValue + "]");
                     }
+                    response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+                    return false;
+                } else {
                     conditionSatisfied = matched.booleanValue();
                 }
-            } else {
-                conditionSatisfied = true;
-            }
-
-            if (!conditionSatisfied) {
-                response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
-                return false;
             }
         }
+        if (hasAsteriskValue && headerValues.hasMoreElements()) {
+            // Note that an If-Match header field with a list value containing 
"*" and other values (including other
+            // instances of "*") is syntactically invalid (therefore not 
allowed to be generated) and furthermore is
+            // unlikely to be interoperable.
+            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+            return false;
+        }
+        if (!conditionSatisfied) {
+            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
+            return false;
+        }
         return true;
     }
 
@@ -2138,14 +2190,16 @@ public class DefaultServlet extends HttpServlet {
      */
     protected boolean checkIfModifiedSince(HttpServletRequest request, 
HttpServletResponse response,
             WebResource resource) {
+
+        long resourceLastModified = resource.getLastModified();
+
         try {
             long headerValue = request.getDateHeader("If-Modified-Since");
-            long lastModified = resource.getLastModified();
             if (headerValue != -1) {
 
                 // If an If-None-Match header has been specified, if modified 
since
                 // is ignored.
-                if ((request.getHeader("If-None-Match") == null) && 
(lastModified < headerValue + 1000)) {
+                if ((request.getHeader("If-None-Match") == null) && 
(resourceLastModified < headerValue + 1000)) {
                     // The entity has not been modified since the date
                     // specified by the client. This is not an error case.
                     response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
@@ -2176,15 +2230,39 @@ public class DefaultServlet extends HttpServlet {
     protected boolean checkIfNoneMatch(HttpServletRequest request, 
HttpServletResponse response, WebResource resource)
             throws IOException {
 
-        String headerValue = request.getHeader("If-None-Match");
-        if (headerValue != null) {
+        String resourceETag = generateETag(resource);
 
-            boolean conditionSatisfied;
+        Enumeration<String> headerValues = request.getHeaders("If-None-Match");
+        if (!headerValues.hasMoreElements()) {
+            return true;
+        }
+        boolean hasAsteriskValue = false;// check existence of special header 
value '*'
+        boolean conditionSatisfied = true;
+        while (headerValues.hasMoreElements()) {
 
-            String resourceETag = generateETag(resource);
-            if (!headerValue.equals("*")) {
-                if (resourceETag == null) {
+            String headerValue = headerValues.nextElement();
+
+            if (headerValue.equals("*")) {
+                hasAsteriskValue = true;
+                if (headerValues.hasMoreElements()) {
                     conditionSatisfied = false;
+                    break;
+                } else {
+                    // asterisk '*' is the only field value.
+                    // RFC9110: If the field value is "*", the condition is 
false if the origin server has a current
+                    // representation for the target resource.
+                    if (resourceETag != null) {
+                        conditionSatisfied = false;
+                    } else {
+                        conditionSatisfied = true;
+                    }
+                    break;
+                }
+            } else {
+                if (resourceETag == null) {
+                    // None of the entity tag matches.
+                    conditionSatisfied = true;
+                    break;
                 } else {
                     // RFC 7232 requires weak comparison for If-None-Match 
headers
                     Boolean matched = EntityTag.compareEntityTag(new 
StringReader(headerValue), true, resourceETag);
@@ -2195,25 +2273,37 @@ public class DefaultServlet extends HttpServlet {
                         response.sendError(HttpServletResponse.SC_BAD_REQUEST);
                         return false;
                     }
-                    conditionSatisfied = matched.booleanValue();
+                    if (matched.booleanValue()) {
+                        // RFC9110: If the field value is a list of entity 
tags, the condition is false if one of the
+                        // listed tags
+                        // matches the entity tag of the selected 
representation.
+                        conditionSatisfied = false;
+                        break;
+                    }
                 }
-            } else {
-                conditionSatisfied = true;
             }
 
-            if (conditionSatisfied) {
-                // For GET and HEAD, we should respond with
-                // 304 Not Modified.
-                // For every other method, 412 Precondition Failed is sent
-                // back.
-                if ("GET".equals(request.getMethod()) || 
"HEAD".equals(request.getMethod())) {
-                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
-                    response.setHeader("ETag", resourceETag);
-                } else {
-                    
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
-                }
-                return false;
+        }
+
+        if (hasAsteriskValue && headerValues.hasMoreElements()) {
+            // Note that an If-None-Match header field with a list value 
containing "*" and other values (including
+            // other instances of "*") is syntactically invalid (therefore not 
allowed to be generated) and furthermore
+            // is unlikely to be interoperable.
+            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+            return false;
+        }
+        if (!conditionSatisfied) {
+            // For GET and HEAD, we should respond with
+            // 304 Not Modified.
+            // For every other method, 412 Precondition Failed is sent
+            // back.
+            if ("GET".equals(request.getMethod()) || 
"HEAD".equals(request.getMethod())) {
+                response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+                response.setHeader("ETag", resourceETag);
+            } else {
+                response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
             }
+            return false;
         }
         return true;
     }
@@ -2233,11 +2323,28 @@ public class DefaultServlet extends HttpServlet {
      */
     protected boolean checkIfUnmodifiedSince(HttpServletRequest request, 
HttpServletResponse response,
             WebResource resource) throws IOException {
+
+        long resourceLastModified = resource.getLastModified();
+        if (resourceLastModified <= -1 || request.getHeader("If-Match") != 
null) {
+            // MUST ignore if the resource does not have a modification date 
available.
+            // MUST ignore if the request contains an If-Match header field
+            return true;
+        }
+        Enumeration<String> headerEnum = 
request.getHeaders("If-Unmodified-Since");
+        if (!headerEnum.hasMoreElements()) {
+            // If-Unmodified-Since is not present
+            return true;
+        }
+        headerEnum.nextElement();
+        if (headerEnum.hasMoreElements()) {
+            // If-Unmodified-Since is a list of dates
+            return true;
+        }
+
         try {
-            long lastModified = resource.getLastModified();
             long headerValue = request.getDateHeader("If-Unmodified-Since");
             if (headerValue != -1) {
-                if (lastModified >= (headerValue + 1000)) {
+                if (resourceLastModified >= (headerValue + 1000)) {
                     // The entity has not been modified since the date
                     // specified by the client. This is not an error case.
                     
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
@@ -2251,6 +2358,79 @@ public class DefaultServlet extends HttpServlet {
     }
 
 
+    /**
+     * Check if the if-range condition is satisfied.
+     *
+     * @param request  The servlet request we are processing
+     * @param response The servlet response we are creating
+     * @param resource The resource
+     *
+     * @return <code>true</code> if the resource meets the specified 
condition, and <code>false</code> if the condition
+     *             is not satisfied, resulting in transfer of the new selected 
representation instead of a 412
+     *             (Precondition Failed) response.
+     *
+     * @throws IOException an IO error occurred
+     */
+    protected boolean checkIfRange(HttpServletRequest request, 
HttpServletResponse response, WebResource resource)
+            throws IOException {
+        String resourceETag = generateETag(resource);
+        long resourceLastModified = resource.getLastModified();
+
+        String headerValue = request.getHeader("If-Range");
+        if (headerValue == null) {
+            return true;
+        }
+
+        String rangeHeader = request.getHeader("Range");
+        if (rangeHeader == null || 
!determineRangeRequestsApplicable(resource)) {
+            // Simply ignore If-Range header field
+            return true;
+        }
+
+        long headerValueTime = (-1L);
+        try {
+            headerValueTime = request.getDateHeader("If-Range");
+        } catch (IllegalArgumentException e) {
+            // Ignore
+        }
+
+        if (headerValueTime == (-1L)) {
+            // If the ETag the client gave does not match the entity
+            // etag, then the entire entity is returned.
+            if (resourceETag != null && resourceETag.startsWith("\"") && 
resourceETag.equals(headerValue.trim())) {
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            // unit of HTTP date is second, ignore millisecond part.
+            return resourceLastModified >= headerValueTime && 
resourceLastModified < headerValueTime + 1000;
+        }
+    }
+
+    /**
+     * Checks if range request is supported by server
+     *
+     * @return <code>true</code> server supports range requests feature.
+     */
+    protected boolean isRangeRequestsSupported() {
+        // Range-Requests optional feature is enabled implicitly.
+        return true;
+    }
+
+    /**
+     * Determines if range-request is applicable for the target resource.
+     * <p>
+     * Subclass have an opportunity to customize by overriding this method.
+     *
+     * @param resource the target resource
+     *
+     * @return <code>true</code> only if range requests is supported by both 
the server and the target resource.
+     */
+    protected boolean determineRangeRequestsApplicable(WebResource resource) {
+        return isRangeRequestsSupported() && resource.isFile() && 
resource.exists();
+    }
+
     /**
      * Provides the entity tag (the ETag header) for the given resource. 
Intended to be over-ridden by custom
      * DefaultServlet implementations that wish to use an alternative format 
for the entity tag.
diff --git 
a/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13.java 
b/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13.java
new file mode 100644
index 0000000000..f191e6f027
--- /dev/null
+++ b/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13.java
@@ -0,0 +1,672 @@
+/*
+ * 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.catalina.servlets;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.function.IntPredicate;
+
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.Wrapper;
+import org.apache.catalina.startup.SimpleHttpClient;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.http.FastHttpDateFormat;
+
+public class TestDefaultServletRfc9110Section13 extends TomcatBaseTest {
+
+    @Test
+    public void testPreconditions2_2_1_head0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_ALL, null, null, 
null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
null, null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_IN, null, null, 
null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_NOT_IN, null, 
null, null, null, 412);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID, 
null, null, null, null, 400);
+    }
+
+    @Test
+    public void testPreconditions2_2_1_head1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_ALL, null, null, 
null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
null, null, null, 412);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_IN, null, null, 
null, null, 412);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_NOT_IN, null, 
null, null, null, 412);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID, 
null, null, null, null, 400);
+    }
+
+    @Test
+    public void testPreconditions2_2_2_head0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_LT, null, 
null, null, 412);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT, null, 
null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN, 
null, null, null, 200);
+    }
+
+    @Test
+    public void testPreconditions2_2_2_head1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_LT, null, 
null, null, 412);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT, null, 
null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN, 
null, null, null, 200);
+    }
+
+    @Test
+    public void testPreconditions2_2_3_head0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_IN, null, 
IfPolicy.ETAG_NOT_IN, null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
IfPolicy.ETAG_EXACTLY, null, null, 304);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
IfPolicy.ETAG_ALL, null, null, 304);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_EQ, 
IfPolicy.ETAG_NOT_IN, null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT, 
IfPolicy.ETAG_EXACTLY, null, null, 304);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT, 
IfPolicy.ETAG_ALL, null, null, 304);
+    }
+
+    @Test
+    public void testPreconditions2_2_3_head1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_ALL, null, 
IfPolicy.ETAG_NOT_IN, null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
IfPolicy.ETAG_EXACTLY, null, null, 304,
+                412);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_EQ, 
IfPolicy.ETAG_NOT_IN, null, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT, 
IfPolicy.ETAG_EXACTLY, null, null, 304);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT, 
IfPolicy.ETAG_ALL, null, null, 304);
+    }
+    // @Test
+    // public void testPreconditions2_2_4_head0() throws Exception {
+    // startServer(true);
+    // testPreconditions(Task.HEAD_INDEX_HTML, null, null, null, 
IfPolicy.DATE_EQ, null, 200);
+    // testPreconditions(Task.HEAD_INDEX_HTML, null, null, null, 
IfPolicy.DATE_LT, null, 412);
+    // testPreconditions(Task.HEAD_INDEX_HTML, null, null, null, 
IfPolicy.DATE_GT, null, 200);
+    // testPreconditions(Task.HEAD_INDEX_HTML, null, null, null, 
IfPolicy.DATE_MULTI, null, 200);
+    // }
+
+    @Test
+    public void testPreconditions2_2_4_head1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, null, null, 
IfPolicy.DATE_EQ, null, 304);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, null, null, 
IfPolicy.DATE_LT, null, 200);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, null, null, 
IfPolicy.DATE_GT, null, 304);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, null, null, 
IfPolicy.DATE_MULTI_IN, null, 200);
+
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_ALL, null, 
IfPolicy.ETAG_NOT_IN, IfPolicy.DATE_EQ, null,
+                200);
+        testPreconditions(Task.HEAD_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
IfPolicy.ETAG_EXACTLY, IfPolicy.DATE_GT,
+                null, 304, 412);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_EQ, 
IfPolicy.ETAG_NOT_IN, IfPolicy.DATE_LT, null,
+                200);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT, 
IfPolicy.ETAG_EXACTLY, IfPolicy.DATE_MULTI_IN,
+                null, 304);
+        testPreconditions(Task.HEAD_INDEX_HTML, null, IfPolicy.DATE_GT, 
IfPolicy.ETAG_ALL, IfPolicy.DATE_EQ, null, 304);
+    }
+
+    @Test
+    public void testPreconditions2_2_1_get0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_ALL, null, null, 
null, null, 200);
+    }
+
+    @Test
+    public void testPreconditions2_2_1_get1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_ALL, null, null, 
null, null, 200);
+        testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
null, null, null, 412);
+        testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_IN, null, null, 
null, null, 412);
+        testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_NOT_IN, null, 
null, null, null, 412);
+        testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID, 
null, null, null, null, 400);
+    }
+
+    @Test
+    public void testPreconditions2_2_2_get0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, null, 200);
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_LT, null, 
null, null, 412);
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_GT, null, 
null, null, 200);
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN, 
null, null, null, 200);
+    }
+
+    @Test
+    public void testPreconditions2_2_2_get1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, null, 200);
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_LT, null, 
null, null, 412);
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_GT, null, 
null, null, 200);
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN, 
null, null, null, 200);
+    }
+
+    @Test
+    public void testPreconditions2_2_5_get0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.GET_INDEX_HTML, null, null, null, null, 
IfPolicy.DATE_EQ, true, 206);
+        // if-range: multiple node policy, not defined in RFC 9110.
+        // Currently, tomcat process the first If-Range header simply.
+        // testPreconditions(Task.GET_INDEX_HTML, null, null, null, null, 
IfPolicy.DATE_MULTI_IN, true,200);
+        testPreconditions(Task.GET_INDEX_HTML, null, null, null, null, 
IfPolicy.DATE_SEMANTIC_INVALID, true, 200);
+        testPreconditions(Task.GET_INDEX_HTML, null, null, null, null, 
IfPolicy.ETAG_EXACTLY, true, 206);
+
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, IfPolicy.DATE_EQ, true, 206);
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, IfPolicy.DATE_LT, true, 200);
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, IfPolicy.DATE_GT, true, 200);
+
+        testPreconditions(Task.GET_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, IfPolicy.DATE_EQ, false, 200);
+
+        // Test Range header is present, while if-range is not.
+        testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_ALL, null, null, 
null, null, true, 206);
+        testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
null, null, null, true, 206);
+        testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_IN, null, null, 
null, null, true, 206);
+        testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_NOT_IN, null, 
null, null, null, true, 412);
+        testPreconditions(Task.GET_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID, 
null, null, null, null, true, 400);
+    }
+
+
+    @Test
+    public void testPreconditions2_2_1_post0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_ALL, null, null, 
null, null, 200);
+    }
+
+    @Test
+    public void testPreconditions2_2_1_post1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_ALL, null, null, 
null, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
null, null, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID, 
null, null, null, null, 400);
+    }
+
+    @Test
+    public void testPreconditions2_2_2_post0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_LT, null, 
null, null, false, null,
+                k -> ((k >= 200 && k < 300) || k == 412), -1);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN, 
null, null, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, 
IfPolicy.DATE_SEMANTIC_INVALID, null, null, null, 200);
+    }
+
+    @Test
+    public void testPreconditions2_2_2_post1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_LT, null, 
null, null, false, null,
+                k -> (k >= 200 && k < 300) || k == 412, -1);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_MULTI_IN, 
null, null, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, 
IfPolicy.DATE_SEMANTIC_INVALID, null, null, null, 200);
+    }
+
+    @Test
+    public void testPreconditions2_2_3_post0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, 
IfPolicy.ETAG_NOT_IN, null, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, 
IfPolicy.ETAG_EXACTLY, null, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, IfPolicy.ETAG_ALL, 
null, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_IN, null, 
IfPolicy.ETAG_NOT_IN, null, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
IfPolicy.ETAG_EXACTLY, null, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
IfPolicy.ETAG_ALL, null, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, 
IfPolicy.ETAG_NOT_IN, null, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_GT, 
IfPolicy.ETAG_EXACTLY, null, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_GT, 
IfPolicy.ETAG_ALL, null, null, 412);
+    }
+
+    @Test
+    public void testPreconditions2_2_3_post1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, 
IfPolicy.ETAG_NOT_IN, null, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, 
IfPolicy.ETAG_EXACTLY, null, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, IfPolicy.ETAG_ALL, 
null, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_ALL, null, 
IfPolicy.ETAG_NOT_IN, null, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
IfPolicy.ETAG_EXACTLY, null, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
IfPolicy.ETAG_ALL, null, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, 
IfPolicy.ETAG_NOT_IN, null, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_GT, 
IfPolicy.ETAG_EXACTLY, null, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_GT, 
IfPolicy.ETAG_ALL, null, null, 412);
+    }
+
+    @Test
+    public void testPreconditions2_2_4_post1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, null, 
IfPolicy.DATE_EQ, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, null, 
IfPolicy.DATE_LT, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, null, 
IfPolicy.DATE_GT, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, null, 
IfPolicy.DATE_MULTI_IN, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, 
IfPolicy.ETAG_NOT_IN, IfPolicy.DATE_EQ, null, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, 
IfPolicy.ETAG_EXACTLY, IfPolicy.DATE_LT, null, 412);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, IfPolicy.ETAG_ALL, 
IfPolicy.DATE_MULTI_IN, null, 412);
+    }
+
+    @Test
+    public void testPreconditions2_2_5_post0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, null, null, 
IfPolicy.DATE_EQ, true, 200);
+        // if-range: multiple node policy, not defined in RFC 9110.
+        // Currently, tomcat process the first If-Range header simply.
+        // testPreconditions(Task.GET_INDEX_HTML, null, null, null, null, 
IfPolicy.DATE_MULTI_IN, true,200);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, null, null, 
IfPolicy.DATE_SEMANTIC_INVALID, true, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, null, null, null, 
IfPolicy.ETAG_EXACTLY, true, 200);
+
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, IfPolicy.DATE_EQ, true, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, IfPolicy.DATE_LT, true, 200);
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, IfPolicy.DATE_GT, true, 200);
+
+        testPreconditions(Task.POST_INDEX_HTML, null, IfPolicy.DATE_EQ, null, 
null, IfPolicy.DATE_EQ, false, 200);
+
+        // Test Range header is present, while if-range is not.
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_ALL, null, null, 
null, null, true, 200);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_EXACTLY, null, 
null, null, null, true, 200);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_IN, null, null, 
null, null, true, 200);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_NOT_IN, null, 
null, null, null, true, 412);
+        testPreconditions(Task.POST_INDEX_HTML, IfPolicy.ETAG_SYNTAX_INVALID, 
null, null, null, null, true, 400);
+    }
+
+    @Ignore
+    @Test
+    public void testPreconditions2_2_1_put0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_ALL, null, null, 
null, null,
+                HttpServletResponse.SC_NO_CONTENT);
+        testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_IN, null, null, 
null, null,
+                HttpServletResponse.SC_NO_CONTENT);
+        testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_NOT_IN, null, 
null, null, null, 412);
+        testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_SYNTAX_INVALID, 
null, null, null, null, 400);
+
+        testPreconditions(Task.PUT_NEW_TXT, null, null, null, null, null, 
HttpServletResponse.SC_CREATED);
+    }
+
+    @Ignore
+    @Test
+    public void testPreconditions2_2_1_put1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_ALL, null, null, 
null, null,
+                HttpServletResponse.SC_NO_CONTENT);
+        testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_SYNTAX_INVALID, 
null, null, null, null, 400);
+        testPreconditions(Task.PUT_EXIST_TXT, IfPolicy.ETAG_EXACTLY, null, 
null, null, null, 412);
+    }
+
+    @Ignore
+    @Test
+    public void testPreconditions2_2_1_delete0() throws Exception {
+        startServer(true);
+        testPreconditions(Task.DELETE_EXIST1_TXT, IfPolicy.ETAG_ALL, null, 
null, null, null,
+                HttpServletResponse.SC_NO_CONTENT);
+        testPreconditions(Task.DELETE_EXIST2_TXT, IfPolicy.ETAG_IN, null, 
null, null, null,
+                HttpServletResponse.SC_NO_CONTENT);
+        testPreconditions(Task.DELETE_EXIST3_TXT, IfPolicy.ETAG_NOT_IN, null, 
null, null, null, 412);
+        testPreconditions(Task.DELETE_EXIST4_TXT, 
IfPolicy.ETAG_SYNTAX_INVALID, null, null, null, null, 400);
+
+        testPreconditions(Task.DELETE_NOT_EXIST_TXT, null, null, null, null, 
null, 404);
+    }
+
+    @Ignore
+    @Test
+    public void testPreconditions2_2_1_delete1() throws Exception {
+        startServer(false);
+        testPreconditions(Task.DELETE_EXIST1_TXT, IfPolicy.ETAG_ALL, null, 
null, null, null,
+                HttpServletResponse.SC_NO_CONTENT);
+        testPreconditions(Task.DELETE_EXIST3_TXT, IfPolicy.ETAG_EXACTLY, null, 
null, null, null, 412);
+        testPreconditions(Task.DELETE_EXIST2_TXT, 
IfPolicy.ETAG_SYNTAX_INVALID, null, null, null, null, 400);
+    }
+
+    enum HTTP_METHOD {
+        GET,
+        PUT,
+        DELETE,
+        POST,
+        HEAD
+    }
+
+    enum Task {
+        HEAD_INDEX_HTML(HTTP_METHOD.HEAD, "/index.html"),
+        HEAD_404_HTML(HTTP_METHOD.HEAD, "/sc_404.html"),
+
+        GET_INDEX_HTML(HTTP_METHOD.GET, "/index.html"),
+        GET_404_HTML(HTTP_METHOD.GET, "/sc_404.html"),
+
+        POST_INDEX_HTML(HTTP_METHOD.POST, "/index.html"),
+        POST_404_HTML(HTTP_METHOD.POST, "/sc_404.html"),
+
+        PUT_EXIST_TXT(HTTP_METHOD.PUT, "/put_exist.txt"),
+        PUT_NEW_TXT(HTTP_METHOD.PUT, "/put_new.txt"),
+
+        DELETE_EXIST_TXT(HTTP_METHOD.DELETE, "/delete_exist.txt"),
+        DELETE_EXIST1_TXT(HTTP_METHOD.DELETE, "/delete_exist1.txt"),
+        DELETE_EXIST2_TXT(HTTP_METHOD.DELETE, "/delete_exist2.txt"),
+        DELETE_EXIST3_TXT(HTTP_METHOD.DELETE, "/delete_exist3.txt"),
+        DELETE_EXIST4_TXT(HTTP_METHOD.DELETE, "/delete_exist4.txt"),
+        DELETE_NOT_EXIST_TXT(HTTP_METHOD.DELETE, "/delete_404.txt");
+
+        HTTP_METHOD m;
+        String uri;
+
+        Task(HTTP_METHOD m, String uri) {
+            this.m = m;
+            this.uri = uri;
+        }
+
+        @Override
+        public String toString() {
+            return m.name() + " " + uri;
+        }
+    }
+
+    enum IfPolicy {
+        ETAG_EXACTLY,
+        ETAG_IN,
+        ETAG_ALL,
+        ETAG_NOT_IN,
+        ETAG_SYNTAX_INVALID,
+        /**
+         * Condition header value of http date is equivalent to actual 
resource lastModified date
+         */
+        DATE_EQ,
+        /**
+         * Condition header value of http date is greater(later) than actual 
resource lastModified date
+         */
+        DATE_GT,
+        /**
+         * Condition header value of http date is less(earlier) than actual 
resource lastModified date
+         */
+        DATE_LT,
+        DATE_MULTI_IN,
+        /**
+         * not a valid HTTP-date
+         */
+        DATE_SEMANTIC_INVALID;
+    }
+
+    enum IfType {
+        ifMatch("If-Match"), // ETag strong comparison
+        ifUnmodifiedSince("If-Unmodified-Since"),
+        ifNoneMatch("If-None-Match"), // ETag weak comparison
+        ifModifiedSince("If-Modified-Since"),
+        ifRange("If-Range"); // ETag strong comparison
+
+        private String header;
+
+        IfType(String header) {
+            this.header = header;
+        }
+
+        public String value() {
+            return this.header;
+        }
+    }
+
+    protected List<String> genETagCondtion(String strongETag, String weakETag, 
IfPolicy policy) {
+        List<String> headerValues = new ArrayList<String>();
+        switch (policy) {
+            case ETAG_ALL:
+                headerValues.add("*");
+                break;
+            case ETAG_EXACTLY:
+                if (strongETag != null) {
+                    headerValues.add(strongETag);
+                } else {
+                    // Should not happen
+                    throw new IllegalArgumentException("strong etag not 
found!");
+                }
+                break;
+            case ETAG_IN:
+                headerValues.add("\"1a2b3c4d\"");
+                headerValues.add(weakETag + "," + strongETag + ",W/\"*\"");
+                headerValues.add("\"abcdefg\"");
+                break;
+            case ETAG_NOT_IN:
+                if (weakETag != null && weakETag.length() > 8) {
+                    headerValues.add(weakETag.substring(0, 3) + 
"XXXXX"+weakETag.substring(8));
+                }
+                if (strongETag != null && strongETag.length() > 6) {
+                    headerValues.add(strongETag.substring(0, 1) + 
"XXXXX"+strongETag.substring(6));
+                }
+                break;
+            case ETAG_SYNTAX_INVALID:
+                headerValues.add("*");
+                headerValues.add("W/\"1abcd\"");
+                break;
+            default:
+                break;
+        }
+        return headerValues;
+    }
+
+    protected List<String> genDateCondtion(long lastModifiedTimestamp, 
IfPolicy policy) {
+        List<String> headerValues = new ArrayList<String>();
+        if (lastModifiedTimestamp <= 0) {
+            return headerValues;
+        }
+        switch (policy) {
+            case DATE_EQ:
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
+                break;
+            case DATE_GT:
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
+                break;
+            case DATE_LT:
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
+                break;
+            case DATE_MULTI_IN:
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
+                break;
+            case DATE_SEMANTIC_INVALID:
+                headerValues.add("2024.12.09 GMT");
+                break;
+            default:
+                break;
+        }
+        return headerValues;
+    }
+
+    protected void wrapperHeaders(Map<String,List<String>> headers, String 
resourceETag, long lastModified,
+            IfPolicy policy, IfType type) {
+        Objects.requireNonNull(type);
+        if (policy == null) {
+            return;
+        }
+        List<String> headerValues = new ArrayList<String>();
+        String weakETag = resourceETag;
+        String strongETag = resourceETag;
+        if (resourceETag != null) {
+            if (resourceETag.startsWith("W/")) {
+                strongETag = resourceETag.substring(2);
+            } else {
+                weakETag = "W/" + resourceETag;
+            }
+        }
+
+        List<String> eTagConditions = genETagCondtion(strongETag, weakETag, 
policy);
+        if (!eTagConditions.isEmpty()) {
+            headerValues.addAll(eTagConditions);
+        }
+
+        List<String> dateConditions = genDateCondtion(lastModified, policy);
+        if (!dateConditions.isEmpty()) {
+            headerValues.addAll(dateConditions);
+        }
+
+        if (!headerValues.isEmpty()) {
+            headers.put(type.value(), headerValues);
+        }
+    }
+
+    private File tempDocBase = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        tempDocBase = 
Files.createTempDirectory(getTemporaryDirectory().toPath(), 
"conditional").toFile();
+        long lastModified = FastHttpDateFormat.parseDate("Fri, 06 Dec 2024 
00:00:00 GMT");
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), "index.html"), 
"<html><body>Index</body></html>".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"index.html").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), "put_exist.txt"), 
"put_exist_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"put_exist.txt").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist.txt"), "delete_exist_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist.txt").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist1.txt"), "delete_exist1_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist1.txt").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist2.txt"), "delete_exist2_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist2.txt").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist3.txt"), "delete_exist3_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist3.txt").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist4.txt"), "delete_exist4_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist4.txt").toFile().setLastModified(lastModified);
+
+    }
+
+    protected void startServer(boolean resourceHasStrongETag) throws Exception 
{
+        Tomcat tomcat = getTomcatInstance();
+        Context ctxt = tomcat.addContext("", tempDocBase.getAbsolutePath());
+
+        Wrapper w = Tomcat.addServlet(ctxt, "default", 
DefaultServlet.class.getName());
+        w.addInitParameter("readonly", "false");
+        w.addInitParameter("allowPartialPut", Boolean.toString(true));
+        w.addInitParameter("useStrongETags", 
Boolean.toString(resourceHasStrongETag));
+        ctxt.addServletMappingDecoded("/", "default");
+
+        tomcat.start();
+    }
+
+
+    protected void testPreconditions(Task task, IfPolicy ifMatchHeader, 
IfPolicy ifUnmodifiedSinceHeader,
+            IfPolicy ifNoneMatchHeader, IfPolicy ifModifiedSinceHeader, 
IfPolicy ifRangeHeader, boolean autoRangeHeader,
+            String message, IntPredicate p, int... scExpected) throws 
Exception {
+        Assert.assertNotNull(task);
+
+
+        Map<String,List<String>> requestHeaders = new HashMap<>();
+
+        Map<String,List<String>> responseHeaders = new HashMap<>();
+
+        String etag = null;
+        long lastModified = -1;
+        String uri = "http://localhost:"; + getPort() + task.uri;
+        // Try head to receives etag and lastModified Date
+        int sc = headUrl(uri, new ByteChunk(), responseHeaders);
+        if (sc == 200) {
+            etag = getSingleHeader("ETag", responseHeaders);
+            String dt = getSingleHeader("Last-Modified", responseHeaders);
+            if (dt != null && dt.length() > 0) {
+                lastModified = FastHttpDateFormat.parseDate(dt);
+            }
+        }
+
+        wrapperHeaders(requestHeaders, etag, lastModified, ifMatchHeader, 
IfType.ifMatch);
+        wrapperHeaders(requestHeaders, etag, lastModified, 
ifModifiedSinceHeader, IfType.ifModifiedSince);
+        wrapperHeaders(requestHeaders, etag, lastModified, ifNoneMatchHeader, 
IfType.ifNoneMatch);
+        wrapperHeaders(requestHeaders, etag, lastModified, 
ifUnmodifiedSinceHeader, IfType.ifUnmodifiedSince);
+        wrapperHeaders(requestHeaders, etag, lastModified, ifRangeHeader, 
IfType.ifRange);
+        responseHeaders.clear();
+        sc = 0;
+        SimpleHttpClient client = null;
+        client = new SimpleHttpClient() {
+
+            @Override
+            public boolean isResponseBodyOK() {
+                return true;
+            }
+        };
+        client.setPort(getPort());
+        StringBuffer curl = new StringBuffer();
+        curl.append(task.m.name() + " " + task.uri + " HTTP/1.1" + 
SimpleHttpClient.CRLF + "Host: localhost" + SimpleHttpClient.CRLF +
+                "Connection: Close" + SimpleHttpClient.CRLF);
+
+        for (Entry<String,List<String>> e : requestHeaders.entrySet()) {
+            for (String v : e.getValue()) {
+                curl.append(e.getKey() + ": " + v + SimpleHttpClient.CRLF);
+            }
+        }
+        if (autoRangeHeader) {
+            curl.append("Range: bytes=0-10" + SimpleHttpClient.CRLF);
+        }
+        curl.append("Content-Length: 6" + SimpleHttpClient.CRLF);
+        curl.append(SimpleHttpClient.CRLF);
+
+        curl.append("PUT_v2");
+        client.setRequest(new String[] { curl.toString() });
+        client.connect();
+        client.processRequest();
+        for (String e : client.getResponseHeaders()) {
+            Assert.assertTrue("Separator ':' expected and not the last char of 
response header field `" + e + "`",
+                    e.contains(":") && e.indexOf(':') < e.length() - 1);
+            String name = e.substring(0, e.indexOf(':'));
+            String value = e.substring(e.indexOf(':') + 1);
+            responseHeaders.computeIfAbsent(name, k -> new 
ArrayList<String>()).add(value);
+        }
+        sc = client.getStatusCode();
+        if (message == null) {
+            message = "Unexpected status code:`" + sc + "`";
+        }
+        boolean test = false;
+        boolean usePredicate = false;
+        if (scExpected != null && scExpected.length > 0 && scExpected[0] >= 
100) {
+            test = Arrays.binarySearch(scExpected, sc) >= 0;
+        } else {
+            usePredicate = true;
+            test = p.test(sc);
+        }
+        String scExpectation = usePredicate ? "IntPredicate" : 
Arrays.toString(scExpected);
+        Assert.assertTrue(
+                "Failure - sc expected:%s, sc actual:%d, %s, task:%s, \ntarget 
resource:(%s,%s), \nreq headers: %s, \nresp headers: %s"
+                        .formatted(scExpectation, sc, message, task, etag, 
FastHttpDateFormat.formatDate(lastModified),
+                                requestHeaders.toString(), 
responseHeaders.toString()),
+                test);
+    }
+
+    protected void testPreconditions(Task task, IfPolicy ifMatchHeader, 
IfPolicy ifUnmodifiedSinceHeader,
+            IfPolicy ifNoneMatchHeader, IfPolicy ifModifiedSinceHeader, 
IfPolicy ifRangeHeader, int... scExpected)
+            throws Exception {
+        testPreconditions(task, ifMatchHeader, ifUnmodifiedSinceHeader, 
ifNoneMatchHeader, ifModifiedSinceHeader,
+                ifRangeHeader, false, scExpected);
+    }
+
+    protected void testPreconditions(Task task, IfPolicy ifMatchHeader, 
IfPolicy ifUnmodifiedSinceHeader,
+            IfPolicy ifNoneMatchHeader, IfPolicy ifModifiedSinceHeader, 
IfPolicy ifRangeHeader, boolean autoRangeHeader,
+            int... scExpected) throws Exception {
+        testPreconditions(task, ifMatchHeader, ifUnmodifiedSinceHeader, 
ifNoneMatchHeader, ifModifiedSinceHeader,
+                ifRangeHeader, autoRangeHeader, null, null, scExpected);
+    }
+}
diff --git 
a/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13Parameterized.java
 
b/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13Parameterized.java
new file mode 100644
index 0000000000..192b9ffc75
--- /dev/null
+++ 
b/test/org/apache/catalina/servlets/TestDefaultServletRfc9110Section13Parameterized.java
@@ -0,0 +1,433 @@
+/*
+ * 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.catalina.servlets;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.function.IntPredicate;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.Wrapper;
+import org.apache.catalina.startup.SimpleHttpClient;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.http.FastHttpDateFormat;
+
+/**
+ * This test case is used to verify RFC 9110 Section 13. Conditional Requests.
+ */
+@RunWith(Parameterized.class)
+public class TestDefaultServletRfc9110Section13Parameterized extends 
TomcatBaseTest {
+    @Parameter(0)
+    public boolean useStrongETags;
+    @Parameter(1)
+    public Task task;
+    @Parameter(2)
+    public IfPolicy ifMatchHeader;
+    @Parameter(3)
+    public IfPolicy ifUnmodifiedSinceHeader;
+    @Parameter(4)
+    public IfPolicy ifNoneMatchHeader;
+    @Parameter(5)
+    public IfPolicy ifModifiedSinceHeader;
+    @Parameter(6)
+    public IfPolicy ifRangeHeader;
+    @Parameter(7)
+    public boolean autoRangeHeader;
+    @Parameter(8)
+    public IntPredicate p;
+    @Parameter(9)
+    public int[] scExpected;
+
+    @Parameterized.Parameters(name = "{index} resource-strong [{0}], 
matchHeader [{1}]")
+    public static Collection<Object[]> parameters() {
+        List<Object[]> parameterSets = new ArrayList<>();
+        // testPreconditions_rfc9110_13_2_2_1_head0
+        parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, 
IfPolicy.ETAG_ALL, null, null, null, null, false,
+                null, new int[] { 200 } });
+        parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, 
IfPolicy.ETAG_EXACTLY, null, null, null, null,
+                false, null, new int[] { 200 } });
+        parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, 
IfPolicy.ETAG_IN, null, null, null, null, false,
+                null, new int[] { 200 } });
+        parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, 
IfPolicy.ETAG_NOT_IN, null, null, null, null,
+                false, null, new int[] { 412 } });
+        parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, 
IfPolicy.ETAG_SYNTAX_INVALID, null, null, null,
+                null, false, null, new int[] { 400 } });
+
+        parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, 
IfPolicy.ETAG_ALL, null, null, null, null, false,
+                null, new int[] { 200 } });
+        parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, 
IfPolicy.ETAG_EXACTLY, null, null, null, null,
+                false, null, new int[] { 412 } });
+        parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, 
IfPolicy.ETAG_IN, null, null, null, null, false,
+                null, new int[] { 412 } });
+        parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, 
IfPolicy.ETAG_NOT_IN, null, null, null, null,
+                false, null, new int[] { 412 } });
+        parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, 
IfPolicy.ETAG_SYNTAX_INVALID, null, null, null,
+                null, false, null, new int[] { 400 } });
+
+        parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, null, 
IfPolicy.DATE_EQ, null, null, null, false,
+                null, new int[] { 200 } });
+        parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, null, 
IfPolicy.DATE_LT, null, null, null, false,
+                null, new int[] { 412 } });
+        parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, null, 
IfPolicy.DATE_GT, null, null, null, false,
+                null, new int[] { 200 } });
+        parameterSets.add(new Object[] { true, Task.HEAD_INDEX_HTML, null, 
IfPolicy.DATE_MULTI_IN, null, null, null,
+                false, null, new int[] { 200 } });
+
+        parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, null, 
IfPolicy.DATE_EQ, null, null, null, false,
+                null, new int[] { 200 } });
+        parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, null, 
IfPolicy.DATE_LT, null, null, null, false,
+                null, new int[] { 412 } });
+        parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, null, 
IfPolicy.DATE_GT, null, null, null, false,
+                null, new int[] { 200 } });
+        parameterSets.add(new Object[] { false, Task.HEAD_INDEX_HTML, null, 
IfPolicy.DATE_MULTI_IN, null, null, null,
+                false, null, new int[] { 200 } });
+
+
+        return parameterSets;
+    }
+
+
+    enum HTTP_METHOD {
+        GET,
+        PUT,
+        DELETE,
+        POST,
+        HEAD
+    }
+
+    enum Task {
+        HEAD_INDEX_HTML(HTTP_METHOD.HEAD, "/index.html"),
+        HEAD_404_HTML(HTTP_METHOD.HEAD, "/sc_404.html"),
+
+        GET_INDEX_HTML(HTTP_METHOD.GET, "/index.html"),
+        GET_404_HTML(HTTP_METHOD.GET, "/sc_404.html"),
+
+        POST_INDEX_HTML(HTTP_METHOD.POST, "/index.html"),
+        POST_404_HTML(HTTP_METHOD.POST, "/sc_404.html"),
+
+        PUT_EXIST_TXT(HTTP_METHOD.PUT, "/put_exist.txt"),
+        PUT_NEW_TXT(HTTP_METHOD.PUT, "/put_new.txt"),
+
+        DELETE_EXIST_TXT(HTTP_METHOD.DELETE, "/delete_exist.txt"),
+        DELETE_EXIST1_TXT(HTTP_METHOD.DELETE, "/delete_exist1.txt"),
+        DELETE_EXIST2_TXT(HTTP_METHOD.DELETE, "/delete_exist2.txt"),
+        DELETE_EXIST3_TXT(HTTP_METHOD.DELETE, "/delete_exist3.txt"),
+        DELETE_EXIST4_TXT(HTTP_METHOD.DELETE, "/delete_exist4.txt"),
+        DELETE_NOT_EXIST_TXT(HTTP_METHOD.DELETE, "/delete_404.txt");
+
+        HTTP_METHOD m;
+        String uri;
+
+        Task(HTTP_METHOD m, String uri) {
+            this.m = m;
+            this.uri = uri;
+        }
+
+        @Override
+        public String toString() {
+            return m.name() + " " + uri;
+        }
+    }
+
+    enum IfPolicy {
+        ETAG_EXACTLY,
+        ETAG_IN,
+        ETAG_ALL,
+        ETAG_NOT_IN,
+        ETAG_SYNTAX_INVALID,
+        /**
+         * Condition header value of http date is equivalent to actual 
resource lastModified date
+         */
+        DATE_EQ,
+        /**
+         * Condition header value of http date is greater(later) than actual 
resource lastModified date
+         */
+        DATE_GT,
+        /**
+         * Condition header value of http date is less(earlier) than actual 
resource lastModified date
+         */
+        DATE_LT,
+        DATE_MULTI_IN,
+        /**
+         * not a valid HTTP-date
+         */
+        DATE_SEMANTIC_INVALID;
+    }
+
+    enum IfType {
+        ifMatch("If-Match"), // ETag strong comparison
+        ifUnmodifiedSince("If-Unmodified-Since"),
+        ifNoneMatch("If-None-Match"), // ETag weak comparison
+        ifModifiedSince("If-Modified-Since"),
+        ifRange("If-Range"); // ETag strong comparison
+
+        private String header;
+
+        IfType(String header) {
+            this.header = header;
+        }
+
+        public String value() {
+            return this.header;
+        }
+    }
+
+    protected List<String> genETagCondtion(String strongETag, String weakETag, 
IfPolicy policy) {
+        List<String> headerValues = new ArrayList<String>();
+        switch (policy) {
+            case ETAG_ALL:
+                headerValues.add("*");
+                break;
+            case ETAG_EXACTLY:
+                if (strongETag != null) {
+                    headerValues.add(strongETag);
+                } else {
+                    // Should not happen
+                    throw new IllegalArgumentException("strong etag not 
found!");
+                }
+                break;
+            case ETAG_IN:
+                headerValues.add("\"1a2b3c4d\"");
+                headerValues.add(weakETag + "," + strongETag + ",W/\"*\"");
+                headerValues.add("\"abcdefg\"");
+                break;
+            case ETAG_NOT_IN:
+                if (weakETag != null && weakETag.length() > 8) {
+                    headerValues.add(weakETag.substring(0, 3) + "XXXXX" + 
weakETag.substring(8));
+                }
+                if (strongETag != null && strongETag.length() > 6) {
+                    headerValues.add(strongETag.substring(0, 1) + "XXXXX" + 
strongETag.substring(6));
+                }
+                break;
+            case ETAG_SYNTAX_INVALID:
+                headerValues.add("*");
+                headerValues.add("W/\"1abcd\"");
+                break;
+            default:
+                break;
+        }
+        return headerValues;
+    }
+
+    protected List<String> genDateCondtion(long lastModifiedTimestamp, 
IfPolicy policy) {
+        List<String> headerValues = new ArrayList<String>();
+        if (lastModifiedTimestamp <= 0) {
+            return headerValues;
+        }
+        switch (policy) {
+            case DATE_EQ:
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
+                break;
+            case DATE_GT:
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
+                break;
+            case DATE_LT:
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
+                break;
+            case DATE_MULTI_IN:
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp - 30000L));
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp));
+                
headerValues.add(FastHttpDateFormat.formatDate(lastModifiedTimestamp + 30000L));
+                break;
+            case DATE_SEMANTIC_INVALID:
+                headerValues.add("2024.12.09 GMT");
+                break;
+            default:
+                break;
+        }
+        return headerValues;
+    }
+
+    protected void wrapperHeaders(Map<String,List<String>> headers, String 
resourceETag, long lastModified,
+            IfPolicy policy, IfType type) {
+        Objects.requireNonNull(type);
+        if (policy == null) {
+            return;
+        }
+        List<String> headerValues = new ArrayList<String>();
+        String weakETag = resourceETag;
+        String strongETag = resourceETag;
+        if (resourceETag != null) {
+            if (resourceETag.startsWith("W/")) {
+                strongETag = resourceETag.substring(2);
+            } else {
+                weakETag = "W/" + resourceETag;
+            }
+        }
+
+        List<String> eTagConditions = genETagCondtion(strongETag, weakETag, 
policy);
+        if (!eTagConditions.isEmpty()) {
+            headerValues.addAll(eTagConditions);
+        }
+
+        List<String> dateConditions = genDateCondtion(lastModified, policy);
+        if (!dateConditions.isEmpty()) {
+            headerValues.addAll(dateConditions);
+        }
+
+        if (!headerValues.isEmpty()) {
+            headers.put(type.value(), headerValues);
+        }
+    }
+
+    private File tempDocBase = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        tempDocBase = 
Files.createTempDirectory(getTemporaryDirectory().toPath(), 
"conditional").toFile();
+        long lastModified = FastHttpDateFormat.parseDate("Fri, 06 Dec 2024 
00:00:00 GMT");
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), "index.html"), 
"<html><body>Index</body></html>".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"index.html").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), "put_exist.txt"), 
"put_exist_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"put_exist.txt").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist.txt"), "delete_exist_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist.txt").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist1.txt"), "delete_exist1_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist1.txt").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist2.txt"), "delete_exist2_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist2.txt").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist3.txt"), "delete_exist3_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist3.txt").toFile().setLastModified(lastModified);
+
+        Files.write(Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist4.txt"), "delete_exist4_v0".getBytes(),
+                StandardOpenOption.CREATE);
+        Path.of(tempDocBase.getAbsolutePath(), 
"delete_exist4.txt").toFile().setLastModified(lastModified);
+
+    }
+
+    @Test
+    public void testPreconditions() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+        Context ctxt = tomcat.addContext("", tempDocBase.getAbsolutePath());
+
+        Wrapper w = Tomcat.addServlet(ctxt, "default", 
DefaultServlet.class.getName());
+        w.addInitParameter("readonly", "false");
+        w.addInitParameter("allowPartialPut", Boolean.toString(true));
+        w.addInitParameter("useStrongETags", Boolean.toString(useStrongETags));
+        ctxt.addServletMappingDecoded("/", "default");
+
+        tomcat.start();
+
+        Assert.assertNotNull(task);
+
+
+        Map<String,List<String>> requestHeaders = new HashMap<>();
+
+        Map<String,List<String>> responseHeaders = new HashMap<>();
+
+        String etag = null;
+        long lastModified = -1;
+        String uri = "http://localhost:"; + getPort() + task.uri;
+        // Try head to receives etag and lastModified Date
+        int sc = headUrl(uri, new ByteChunk(), responseHeaders);
+        if (sc == 200) {
+            etag = getSingleHeader("ETag", responseHeaders);
+            String dt = getSingleHeader("Last-Modified", responseHeaders);
+            if (dt != null && dt.length() > 0) {
+                lastModified = FastHttpDateFormat.parseDate(dt);
+            }
+        }
+
+        wrapperHeaders(requestHeaders, etag, lastModified, ifMatchHeader, 
IfType.ifMatch);
+        wrapperHeaders(requestHeaders, etag, lastModified, 
ifModifiedSinceHeader, IfType.ifModifiedSince);
+        wrapperHeaders(requestHeaders, etag, lastModified, ifNoneMatchHeader, 
IfType.ifNoneMatch);
+        wrapperHeaders(requestHeaders, etag, lastModified, 
ifUnmodifiedSinceHeader, IfType.ifUnmodifiedSince);
+        wrapperHeaders(requestHeaders, etag, lastModified, ifRangeHeader, 
IfType.ifRange);
+        responseHeaders.clear();
+        sc = 0;
+        SimpleHttpClient client = null;
+        client = new SimpleHttpClient() {
+
+            @Override
+            public boolean isResponseBodyOK() {
+                return true;
+            }
+        };
+        client.setPort(getPort());
+        StringBuffer curl = new StringBuffer();
+        curl.append(task.m.name() + " " + task.uri + " HTTP/1.1" + 
SimpleHttpClient.CRLF + "Host: localhost" +
+                SimpleHttpClient.CRLF + "Connection: Close" + 
SimpleHttpClient.CRLF);
+
+        for (Entry<String,List<String>> e : requestHeaders.entrySet()) {
+            for (String v : e.getValue()) {
+                curl.append(e.getKey() + ": " + v + SimpleHttpClient.CRLF);
+            }
+        }
+        if (autoRangeHeader) {
+            curl.append("Range: bytes=0-10" + SimpleHttpClient.CRLF);
+        }
+        curl.append("Content-Length: 6" + SimpleHttpClient.CRLF);
+        curl.append(SimpleHttpClient.CRLF);
+
+        curl.append("PUT_v2");
+        client.setRequest(new String[] { curl.toString() });
+        client.connect();
+        client.processRequest();
+        for (String e : client.getResponseHeaders()) {
+            Assert.assertTrue("Separator ':' expected and not the last char of 
response header field `" + e + "`",
+                    e.contains(":") && e.indexOf(':') < e.length() - 1);
+            String name = e.substring(0, e.indexOf(':'));
+            String value = e.substring(e.indexOf(':') + 1);
+            responseHeaders.computeIfAbsent(name, k -> new 
ArrayList<String>()).add(value);
+        }
+        sc = client.getStatusCode();
+        boolean test = false;
+        boolean usePredicate = false;
+        if (scExpected != null && scExpected.length > 0 && scExpected[0] >= 
100) {
+            test = Arrays.binarySearch(scExpected, sc) >= 0;
+        } else {
+            usePredicate = true;
+            test = p.test(sc);
+        }
+        String scExpectation = usePredicate ? "IntPredicate" : 
Arrays.toString(scExpected);
+        Assert.assertTrue(
+                "Failure - sc expected:%s, sc actual:%d, task:%s, \ntarget 
resource:(%s,%s), \nreq headers: %s, \nresp headers: %s"
+                        .formatted(scExpectation, sc, task, etag, 
FastHttpDateFormat.formatDate(lastModified),
+                                requestHeaders.toString(), 
responseHeaders.toString()),
+                test);
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 4e5ddc1d36..f66ff9eeb9 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -139,6 +139,10 @@
         <code>DataSourcePropertyStore</code> that may be used by the WebDAV
         Servlet. (remm)
       </update>
+      <update>
+        Improve HTTP If headers processing according to RFC 9110. Based on pull
+        request <pr>796</pr> by Chenjp. (remm)
+      </update>
     </changelog>
   </subsection>
   <subsection name="Coyote">


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to