You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by bc...@apache.org on 2020/11/30 19:02:34 UTC

[trafficserver] branch 9.0.x updated (9aa2651 -> 34f29ab)

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

bcall pushed a change to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git.


    from 9aa2651  More release notes updates, this is up to page 22
     new 1ba4304  Add negative caching tests and fixes. (#7361)
     new 7c35ec3  ESI: Ensure gzip header is always initialized (#7360)
     new 34f29ab  Updated ChangeLog

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 CHANGELOG-9.0.0                                    |   3 +
 doc/admin-guide/files/records.config.en.rst        |   4 +-
 doc/admin-guide/performance/index.en.rst           |   4 +-
 doc/admin-guide/plugins/esi.en.rst                 |   8 +-
 mgmt/RecordsConfig.cc                              |   2 +-
 plugins/esi/esi.cc                                 |   7 +-
 plugins/esi/lib/EsiGzip.cc                         |  19 +-
 plugins/esi/lib/EsiGzip.h                          |  27 +-
 proxy/http/HttpConfig.cc                           |   2 +-
 proxy/http/HttpSM.cc                               |   9 +-
 proxy/http/HttpTransact.cc                         |  38 +--
 proxy/http/HttpTransact.h                          |  19 +-
 .../autest-site/verifier_server.test.ext           |   4 +
 tests/gold_tests/cache/negative-caching.test.py    | 163 +++++++++++
 ...negative-caching-300-second-timeout.replay.yaml |  72 +++++
 .../replay/negative-caching-customized.replay.yaml | 164 +++++++++++
 .../replay/negative-caching-default.replay.yaml    | 206 ++++++++++++++
 .../replay/negative-caching-disabled.replay.yaml   | 201 +++++++++++++
 .../replay/negative-caching-no-timeout.replay.yaml |  44 ++-
 .../replay/negative-caching-timeout.replay.yaml}   |  68 +++--
 tests/gold_tests/pluginTest/esi/esi.test.py        | 310 +++++++++++++++++++++
 .../pluginTest/esi/gold/empty_response_body.gold   |  14 +
 tests/gold_tests/pluginTest/esi/gold/esi_body.gold |  12 +
 .../pluginTest/esi/gold/esi_gzipped.gold           |  10 +
 .../pluginTest/esi/gold/esi_headers.gold           |   6 +
 25 files changed, 1324 insertions(+), 92 deletions(-)
 create mode 100644 tests/gold_tests/cache/negative-caching.test.py
 create mode 100644 tests/gold_tests/cache/replay/negative-caching-300-second-timeout.replay.yaml
 create mode 100644 tests/gold_tests/cache/replay/negative-caching-customized.replay.yaml
 create mode 100644 tests/gold_tests/cache/replay/negative-caching-default.replay.yaml
 create mode 100644 tests/gold_tests/cache/replay/negative-caching-disabled.replay.yaml
 copy src/traffic_logstats/tests/test_logstats_summary => tests/gold_tests/cache/replay/negative-caching-no-timeout.replay.yaml (50%)
 mode change 100755 => 100644
 copy tests/gold_tests/{url/uri.replay.yaml => cache/replay/negative-caching-timeout.replay.yaml} (51%)
 create mode 100644 tests/gold_tests/pluginTest/esi/esi.test.py
 create mode 100644 tests/gold_tests/pluginTest/esi/gold/empty_response_body.gold
 create mode 100644 tests/gold_tests/pluginTest/esi/gold/esi_body.gold
 create mode 100644 tests/gold_tests/pluginTest/esi/gold/esi_gzipped.gold
 create mode 100644 tests/gold_tests/pluginTest/esi/gold/esi_headers.gold


[trafficserver] 03/03: Updated ChangeLog

Posted by bc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

bcall pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit 34f29ab7029ed2e9c0010c2e0fe2e591262d42e7
Author: Bryan Call <bc...@apache.org>
AuthorDate: Mon Nov 30 11:02:11 2020 -0800

    Updated ChangeLog
---
 CHANGELOG-9.0.0 | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/CHANGELOG-9.0.0 b/CHANGELOG-9.0.0
index 82e578e..5dc64c4 100644
--- a/CHANGELOG-9.0.0
+++ b/CHANGELOG-9.0.0
@@ -1127,3 +1127,6 @@ Changes with Apache Traffic Server 9.0.0
   #7293 - sphinx for 9.x has to be 2.0.1
   #7294 - Make double Au test more reliable.
   #7299 - Adds a shell script to help build the H3 toolchains
+  #7301 - Updated release notes for 9.0.0 to have QUIC draft 29 support as well
+  #7360 - ESI: Ensure gzip header is always initialized
+  #7361 - Add negative caching tests and fixes.


[trafficserver] 02/03: ESI: Ensure gzip header is always initialized (#7360)

Posted by bc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

bcall pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit 7c35ec320319b61d2869a9cc3d02076b0c3d3f62
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Mon Nov 30 12:45:22 2020 -0600

    ESI: Ensure gzip header is always initialized (#7360)
    
    There were certain code paths that could be reached in which
    stream_finish could be called without stream_encode having been called.
    This would result in the gzip header not being encoded in the stream,
    which would produce malformed gzip content.
    
    (cherry picked from commit 3113501b3b5acb6b8edd6269c6ffa0b52ef5603b)
---
 doc/admin-guide/plugins/esi.en.rst                 |   8 +-
 plugins/esi/esi.cc                                 |   7 +-
 plugins/esi/lib/EsiGzip.cc                         |  19 +-
 plugins/esi/lib/EsiGzip.h                          |  27 +-
 tests/gold_tests/pluginTest/esi/esi.test.py        | 310 +++++++++++++++++++++
 .../pluginTest/esi/gold/empty_response_body.gold   |  14 +
 tests/gold_tests/pluginTest/esi/gold/esi_body.gold |  12 +
 .../pluginTest/esi/gold/esi_gzipped.gold           |  10 +
 .../pluginTest/esi/gold/esi_headers.gold           |   6 +
 9 files changed, 398 insertions(+), 15 deletions(-)

diff --git a/doc/admin-guide/plugins/esi.en.rst b/doc/admin-guide/plugins/esi.en.rst
index 48c3f81..4deeed8 100644
--- a/doc/admin-guide/plugins/esi.en.rst
+++ b/doc/admin-guide/plugins/esi.en.rst
@@ -80,11 +80,11 @@ Enabling ESI
 
 2. There are four options you can add to the above.
 
-- "--private-response" will add private cache control and expires header to the processed ESI document.
-- "--packed-node-support" will enable the support for using packed node, which will improve the performance of parsing
+- ``--private-response`` will add private cache control and expires header to the processed ESI document.
+- ``--packed-node-support`` will enable the support for using packed node, which will improve the performance of parsing
   cached ESI document.
-- "--disable-gzip-output" will disable gzipped output, which will NOT gzip the output anyway.
-- "--first-byte-flush" will enable the first byte flush feature, which will flush content to users as soon as the entire
+- ``--disable-gzip-output`` will disable gzipped output, which will NOT gzip the output anyway.
+- ``--first-byte-flush`` will enable the first byte flush feature, which will flush content to users as soon as the entire
   ESI document is received and parsed without all ESI includes fetched (the flushing will stop at the ESI include markup
   till that include is fetched).
 
diff --git a/plugins/esi/esi.cc b/plugins/esi/esi.cc
index 0a01c9b..000f540 100644
--- a/plugins/esi/esi.cc
+++ b/plugins/esi/esi.cc
@@ -777,7 +777,7 @@ transformData(TSCont contp)
             out_data_len = 0;
             out_data     = "";
           } else {
-            TSDebug(cont_data->debug_tag, "[%s] Compressed document from size %d to %d bytes", __FUNCTION__, out_data_len,
+            TSDebug(cont_data->debug_tag, "[%s] Compressed document from size %d to %d bytes via gzip", __FUNCTION__, out_data_len,
                     static_cast<int>(cdata.size()));
             out_data_len = cdata.size();
             out_data     = cdata.data();
@@ -842,12 +842,9 @@ transformData(TSCont contp)
         if (!cont_data->esi_gzip->stream_encode(out_data, cdata)) {
           TSError("[esi][%s] Error while gzipping content", __FUNCTION__);
         } else {
-          TSDebug(cont_data->debug_tag, "[%s] Compressed document from size %d to %d bytes", __FUNCTION__,
+          TSDebug(cont_data->debug_tag, "[%s] Compressed document from size %d to %d bytes via EsiGzip", __FUNCTION__,
                   static_cast<int>(out_data.size()), static_cast<int>(cdata.size()));
         }
-      }
-
-      if (cont_data->gzip_output) {
         if (TSIOBufferWrite(TSVIOBufferGet(cont_data->output_vio), cdata.data(), cdata.size()) == TS_ERROR) {
           TSError("[esi][%s] Error while writing bytes to downstream VC", __FUNCTION__);
           return 0;
diff --git a/plugins/esi/lib/EsiGzip.cc b/plugins/esi/lib/EsiGzip.cc
index 35855f9..2687bd8 100644
--- a/plugins/esi/lib/EsiGzip.cc
+++ b/plugins/esi/lib/EsiGzip.cc
@@ -30,7 +30,7 @@ using std::string;
 using namespace EsiLib;
 
 EsiGzip::EsiGzip(const char *debug_tag, ComponentBase::Debug debug_func, ComponentBase::Error error_func)
-  : ComponentBase(debug_tag, debug_func, error_func), _downstream_length(0), _total_data_length(0)
+  : ComponentBase(debug_tag, debug_func, error_func), _downstream_length(0), _total_data_length(0), _crc(0)
 {
   // Zlib _zstrm variables are initialized when they are required in runDeflateLoop
   // coverity[uninit_member]
@@ -71,6 +71,7 @@ runDeflateLoop(z_stream &zstrm, int flush, std::string &cdata)
 bool
 EsiGzip::stream_encode(const char *data, int data_len, std::string &cdata)
 {
+  const auto initial_cdata_size = cdata.size();
   if (_downstream_length == 0) {
     cdata.assign(GZIP_HEADER_SIZE, 0); // reserving space for the header
     cdata[0] = MAGIC_BYTE_1;
@@ -102,10 +103,9 @@ EsiGzip::stream_encode(const char *data, int data_len, std::string &cdata)
       return false;
     }
     _crc = crc32(_crc, reinterpret_cast<const Bytef *>(data), data_len);
-    _downstream_length += cdata.size();
     _total_data_length += data_len;
   }
-
+  _downstream_length += cdata.size() - initial_cdata_size;
   deflateEnd(&_zstrm);
 
   return true;
@@ -114,6 +114,17 @@ EsiGzip::stream_encode(const char *data, int data_len, std::string &cdata)
 bool
 EsiGzip::stream_finish(std::string &cdata, int &downstream_length)
 {
+  if (_downstream_length == 0) {
+    // We need to run encode first to get the gzip header inserted.
+    if (!stream_encode(nullptr, 0, cdata)) {
+      return false;
+    }
+  }
+  // Note that a call to stream_encode will update cdata to apply the gzip
+  // header and that call itself will update _downstream_length. Since we don't
+  // want to double count the gzip header bytes, we capture initial_cdata_size
+  // here after any possible call to stream_encode above.
+  const auto initial_cdata_size = cdata.size();
   char buf[BUF_SIZE];
 
   _zstrm.zalloc = Z_NULL;
@@ -136,7 +147,7 @@ EsiGzip::stream_finish(std::string &cdata, int &downstream_length)
   }
   append(cdata, static_cast<uint32_t>(_crc));
   append(cdata, static_cast<int32_t>(_total_data_length));
-  _downstream_length += cdata.size();
+  _downstream_length += cdata.size() - initial_cdata_size;
   downstream_length = _downstream_length;
   return true;
 }
diff --git a/plugins/esi/lib/EsiGzip.h b/plugins/esi/lib/EsiGzip.h
index 3fe9b1f..d6f6869 100644
--- a/plugins/esi/lib/EsiGzip.h
+++ b/plugins/esi/lib/EsiGzip.h
@@ -26,6 +26,7 @@
 #include "ComponentBase.h"
 #include <zlib.h>
 #include <string>
+#include <string_view>
 
 class EsiGzip : private EsiLib::ComponentBase
 {
@@ -34,19 +35,41 @@ public:
 
   ~EsiGzip() override;
 
+  /** Compress the provided content.
+   *
+   * @param[in] data The input data to compress.
+   * @param[in] data_len The length of the input data to compress.
+   * @param[in,out] The result of compressing the input data will be appended
+   *    to cdata.
+   *
+   * @return True if the compression succeeded, false otherwise.
+   */
   bool stream_encode(const char *data, int data_len, std::string &cdata);
 
+  /** A string_view overload of stream_encode. */
   inline bool
-  stream_encode(std::string data, std::string &cdata)
+  stream_encode(std::string_view data, std::string &cdata)
   {
     return stream_encode(data.data(), data.size(), cdata);
   }
 
+  /** Finish the compression stream.
+   *
+   * @param[out] cdata The compressed data is appended to this.
+   * @param[out] downstream_length The total number of compressed stream bytes
+   *    across all calls to stream_encode and stream_finish.
+   *
+   * @return True if the compression succeeded, false otherwise.
+   */
   bool stream_finish(std::string &cdata, int &downstream_length);
 
 private:
-  // int runDeflateLoop(z_stream &zstrm, int flush, std::string &cdata);
+  /** The cumulative total number of bytes for the compressed stream. */
   int _downstream_length;
+
+  /** The cumulative total number of uncompressed bytes that have been
+   * compressed.
+   */
   int _total_data_length;
   z_stream _zstrm;
   uLong _crc;
diff --git a/tests/gold_tests/pluginTest/esi/esi.test.py b/tests/gold_tests/pluginTest/esi/esi.test.py
new file mode 100644
index 0000000..e81450b
--- /dev/null
+++ b/tests/gold_tests/pluginTest/esi/esi.test.py
@@ -0,0 +1,310 @@
+'''
+Test the ESI plugin.
+'''
+#  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.
+
+import os
+
+Test.Summary = '''
+Test the ESI plugin.
+'''
+
+Test.SkipUnless(
+    Condition.PluginExists('esi.so'),
+)
+
+
+class EsiTest():
+    """
+    A class that encapsulates the configuration and execution of a set of EPI
+    test cases.
+    """
+
+    """ static: The same server Process is used across all tests. """
+    _server = None
+
+    """ static: A counter to keep the ATS process names unique across tests. """
+    _ts_counter = 0
+
+    """ static: A counter to keep any output file names unique across tests. """
+    _output_counter = 0
+
+    """ The ATS process for this set of test cases. """
+    _ts = None
+
+    def __init__(self, plugin_config):
+        """
+        Args:
+            plugin_config (str): The config line to place in plugin.config for
+                the ATS process.
+        """
+        if EsiTest._server is None:
+            EsiTest._server = EsiTest._create_server()
+
+        self._ts = EsiTest._create_ats(self, plugin_config)
+
+    @staticmethod
+    def _create_server():
+        """
+        Create and start a server process.
+        """
+        # Configure our server.
+        server = Test.MakeOriginServer("server")
+
+        # Generate the set of ESI responses derived right from our ESI docs.
+        # See:
+        #   doc/admin-guide/plugins/esi.en.rst
+        request_header = {
+            "headers": (
+                "GET /esi.php HTTP/1.1\r\n"
+                "Host: www.example.com\r\n"
+                "Content-Length: 0\r\n\r\n"),
+            "timestamp": "1469733493.993",
+            "body": ""
+        }
+        esi_body = r'''<?php   header('X-Esi: 1'); ?>
+<html>
+<body>
+Hello, <esi:include src="http://www.example.com/date.php"/>
+</body>
+</html>
+'''
+        response_header = {
+            "headers": (
+                "HTTP/1.1 200 OK\r\n"
+                "Content-Type: text/html\r\n"
+                "X-Esi: 1\r\n"
+                "Connection: close\r\n"
+                "Content-Length: {}\r\n"
+                "Cache-Control: max-age=300\r\n"
+                "\r\n".format(len(esi_body))),
+            "timestamp": "1469733493.993",
+            "body": esi_body
+        }
+        server.addResponse("sessionfile.log", request_header, response_header)
+        request_header = {
+            "headers": (
+                "GET /date.php HTTP/1.1\r\n"
+                "Host: www.example.com\r\n"
+                "Content-Length: 0\r\n\r\n"),
+            "timestamp": "1469733493.993",
+            "body": ""
+        }
+        date_body = r'''<?php
+header ("Cache-control: no-cache");
+echo date('l jS \of F Y h:i:s A');
+?>
+'''
+        response_header = {
+            "headers": (
+                "HTTP/1.1 200 OK\r\n"
+                "Content-Type: text/html\r\n"
+                "Connection: close\r\n"
+                "Content-Length: {}\r\n"
+                "Cache-Control: max-age=300\r\n"
+                "\r\n".format(len(date_body))),
+            "timestamp": "1469733493.993",
+            "body": date_body
+        }
+        server.addResponse("sessionfile.log", request_header, response_header)
+        # Verify correct functionality with an empty body.
+        request_header = {
+            "headers": (
+                "GET /expect_empty_body HTTP/1.1\r\n"
+                "Host: www.example.com\r\n"
+                "Content-Length: 0\r\n\r\n"),
+            "timestamp": "1469733493.993",
+            "body": ""
+        }
+        response_header = {
+            "headers": (
+                "HTTP/1.1 200 OK\r\n"
+                "X-ESI: On\r\n"
+                "Content-Length: 0\r\n"
+                "Connection: close\r\n"
+                "Content-Type: text/html; charset=UTF-8\r\n"
+                "\r\n"),
+            "timestamp": "1469733493.993",
+            "body": ""
+        }
+        server.addResponse("sessionfile.log", request_header, response_header)
+
+        # Create a run to start the server.
+        tr = Test.AddTestRun("Start the server.")
+        tr.Processes.Default.StartBefore(server)
+        tr.Processes.Default.Command = "echo starting the server"
+        tr.Processes.Default.ReturnCode = 0
+        tr.StillRunningAfter = server
+
+        return server
+
+    @staticmethod
+    def _create_ats(self, plugin_config):
+        """
+        Create and start an ATS process.
+        """
+        EsiTest._ts_counter += 1
+
+        # Configure ATS with a vanilla ESI plugin configuration.
+        ts = Test.MakeATSProcess("ts{}".format(EsiTest._ts_counter))
+        ts.Disk.records_config.update({
+            'proxy.config.diags.debug.enabled': 1,
+            'proxy.config.diags.debug.tags': 'http|plugin_esi',
+        })
+        ts.Disk.remap_config.AddLine(
+            'map http://www.example.com/ http://127.0.0.1:{0}'.format(EsiTest._server.Variables.Port)
+        )
+        ts.Disk.plugin_config.AddLine(plugin_config)
+
+        # Create a run to start the ATS process.
+        tr = Test.AddTestRun("Start the ATS process.")
+        tr.Processes.Default.StartBefore(ts)
+        tr.Processes.Default.Command = "echo starting ATS"
+        tr.Processes.Default.ReturnCode = 0
+        tr.StillRunningAfter = ts
+        return ts
+
+    def run_cases_expecting_gzip(self):
+        # Test 1: Verify basic ESI functionality.
+        tr = Test.AddTestRun("First request for esi.php: not cached")
+        tr.Processes.Default.Command = \
+            ('curl http://127.0.0.1:{0}/esi.php -H"Host: www.example.com" '
+             '-H"Accept: */*" --verbose'.format(
+                 self._ts.Variables.port))
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.stderr = "gold/esi_headers.gold"
+        tr.Processes.Default.Streams.stdout = "gold/esi_body.gold"
+        tr.StillRunningAfter = self._server
+        tr.StillRunningAfter = self._ts
+
+        # Test 2: Repeat the above, should now be cached.
+        tr = Test.AddTestRun("Second request for esi.php: will be cached")
+        tr.Processes.Default.Command = \
+            ('curl http://127.0.0.1:{0}/esi.php -H"Host: www.example.com" '
+             '-H"Accept: */*" --verbose'.format(
+                 self._ts.Variables.port))
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.stderr = "gold/esi_headers.gold"
+        tr.Processes.Default.Streams.stdout = "gold/esi_body.gold"
+        tr.StillRunningAfter = self._server
+        tr.StillRunningAfter = self._ts
+
+        # Test 3: Verify the ESI plugin can gzip a response when the client accepts it.
+        tr = Test.AddTestRun("Verify the ESI plugin can gzip a response")
+        EsiTest._output_counter += 1
+        unzipped_body_file = os.path.join(
+            tr.RunDirectory,
+            "non_empty_curl_output_{}".format(EsiTest._output_counter))
+        gzipped_body_file = unzipped_body_file + ".gz"
+        tr.Processes.Default.Command = \
+            ('curl http://127.0.0.1:{0}/esi.php -H"Host: www.example.com" '
+             '-H "Accept-Encoding: gzip" -H"Accept: */*" --verbose --output {1}'.format(
+                 self._ts.Variables.port, gzipped_body_file))
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Ready = When.FileExists(gzipped_body_file)
+        tr.Processes.Default.Streams.stderr = "gold/esi_gzipped.gold"
+        tr.StillRunningAfter = self._server
+        tr.StillRunningAfter = self._ts
+        zipped_body_disk_file = tr.Disk.File(gzipped_body_file)
+        zipped_body_disk_file.Exists = True
+
+        # Now, unzip the file and make sure its size is the expected body.
+        tr = Test.AddTestRun("Verify the file uzips to the expected body.")
+        tr.Processes.Default.Command = "gunzip {}".format(gzipped_body_file)
+        tr.Processes.Default.Ready = When.FileExists(unzipped_body_file)
+        tr.Processes.Default.ReturnCode = 0
+        unzipped_body_disk_file = tr.Disk.File(unzipped_body_file)
+        unzipped_body_disk_file.Content = "gold/esi_body.gold"
+
+        # Test 4: Verify correct handling of a gzipped empty response body.
+        tr = Test.AddTestRun("Verify we can handle an empty response.")
+        EsiTest._output_counter += 1
+        empty_body_file = os.path.join(
+            tr.RunDirectory,
+            "empty_curl_output_{}".format(EsiTest._output_counter))
+        gzipped_empty_body = empty_body_file + ".gz"
+        tr.Processes.Default.Command = \
+            ('curl http://127.0.0.1:{0}/expect_empty_body -H"Host: www.example.com" '
+             '-H"Accept-Encoding: gzip" -H"Accept: */*" --verbose --output {1}'.format(
+                 self._ts.Variables.port, gzipped_empty_body))
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Ready = When.FileExists(gzipped_empty_body)
+        tr.Processes.Default.Streams.stderr = "gold/empty_response_body.gold"
+        tr.StillRunningAfter = self._server
+        tr.StillRunningAfter = self._ts
+        # The gzipped output file should be greater than 0, even though 0 bytes are
+        # compressed.
+        gz_disk_file = tr.Disk.File(gzipped_empty_body)
+        gz_disk_file.Size = Testers.GreaterThan(0)
+
+        # Now, unzip the file and make sure its size is the original 0 size body.
+        tr = Test.AddTestRun("Verify the file uzips to a zero sized file.")
+        tr.Processes.Default.Command = "gunzip {}".format(gzipped_empty_body)
+        tr.Processes.Default.Ready = When.FileExists(empty_body_file)
+        tr.Processes.Default.ReturnCode = 0
+        unzipped_disk_file = tr.Disk.File(empty_body_file)
+        unzipped_disk_file.Size = 0
+
+    def run_cases_expecting_no_gzip(self):
+        # Test 1: Run an ESI test where the client does not accept gzip.
+        tr = Test.AddTestRun("First request for esi.php: gzip not accepted.")
+        tr.Processes.Default.Command = \
+            ('curl http://127.0.0.1:{0}/esi.php -H"Host: www.example.com" '
+             '-H"Accept: */*" --verbose'.format(
+                 self._ts.Variables.port))
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.stderr = "gold/esi_headers.gold"
+        tr.Processes.Default.Streams.stdout = "gold/esi_body.gold"
+        tr.StillRunningAfter = self._server
+        tr.StillRunningAfter = self._ts
+
+        # Test 2: Verify the ESI plugin does not gzip the response even if the
+        # client accepts the gzip encoding.
+        tr = Test.AddTestRun("Verify the ESI plugin refuses to gzip responses with -–disable-gzip-output")
+        tr.Processes.Default.Command = \
+            ('curl http://127.0.0.1:{0}/esi.php -H"Host: www.example.com" '
+             '-H "Accept-Encoding: gzip" -H"Accept: */*" --verbose'.format(
+                 self._ts.Variables.port))
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.stderr = "gold/esi_headers.gold"
+        tr.Processes.Default.Streams.stdout = "gold/esi_body.gold"
+        tr.StillRunningAfter = self._server
+        tr.StillRunningAfter = self._ts
+
+
+#
+# Configure and run the test cases.
+#
+
+# Run the tests with ESI configured with no parameters.
+vanilla_test = EsiTest(plugin_config='esi.so')
+vanilla_test.run_cases_expecting_gzip()
+
+# For these test cases, the behavior should remain the same with
+# --first-byte-flush set.
+first_byte_flush_test = EsiTest(plugin_config='esi.so --first-byte-flush')
+first_byte_flush_test.run_cases_expecting_gzip()
+
+# For these test cases, the behavior should remain the same with
+# --packed-node-support set.
+packed_node_support_test = EsiTest(plugin_config='esi.so --packed-node-support')
+packed_node_support_test.run_cases_expecting_gzip()
+
+# Run a set of cases verifying that the plugin does not zip content if
+# --disable-gzip-output is set.
+gzip_disabled_test = EsiTest(plugin_config='esi.so --disable-gzip-output')
+gzip_disabled_test.run_cases_expecting_no_gzip()
diff --git a/tests/gold_tests/pluginTest/esi/gold/empty_response_body.gold b/tests/gold_tests/pluginTest/esi/gold/empty_response_body.gold
new file mode 100644
index 0000000..994a61e
--- /dev/null
+++ b/tests/gold_tests/pluginTest/esi/gold/empty_response_body.gold
@@ -0,0 +1,14 @@
+``
+> GET /expect_empty_body HTTP/1.1
+> Host: www.example.com
+> User-Agent: ``
+> Accept-Encoding: gzip
+> Accept: */*
+``
+< HTTP/1.1 200 OK
+< Content-Type: text/html; charset=UTF-8
+``
+< Content-Length: 20
+``
+< Content-Encoding: gzip
+``
diff --git a/tests/gold_tests/pluginTest/esi/gold/esi_body.gold b/tests/gold_tests/pluginTest/esi/gold/esi_body.gold
new file mode 100644
index 0000000..a087e56
--- /dev/null
+++ b/tests/gold_tests/pluginTest/esi/gold/esi_body.gold
@@ -0,0 +1,12 @@
+``
+<?php   header('X-Esi: 1'); ?>
+<html>
+<body>
+Hello, <?php
+header ("Cache-control: no-cache");
+echo date('l jS \of F Y h:i:s A');
+?>
+
+</body>
+</html>
+``
diff --git a/tests/gold_tests/pluginTest/esi/gold/esi_gzipped.gold b/tests/gold_tests/pluginTest/esi/gold/esi_gzipped.gold
new file mode 100644
index 0000000..025d13b
--- /dev/null
+++ b/tests/gold_tests/pluginTest/esi/gold/esi_gzipped.gold
@@ -0,0 +1,10 @@
+``
+> GET /esi.php HTTP/1.1
+``
+> Accept-Encoding: gzip
+> Accept: */*
+``
+< HTTP/1.1 200 OK
+``
+< Content-Encoding: gzip
+``
diff --git a/tests/gold_tests/pluginTest/esi/gold/esi_headers.gold b/tests/gold_tests/pluginTest/esi/gold/esi_headers.gold
new file mode 100644
index 0000000..4e9d577
--- /dev/null
+++ b/tests/gold_tests/pluginTest/esi/gold/esi_headers.gold
@@ -0,0 +1,6 @@
+``
+> GET /esi.php HTTP/1.1
+``
+< HTTP/1.1 200 OK
+< Content-Type: text/html
+``


[trafficserver] 01/03: Add negative caching tests and fixes. (#7361)

Posted by bc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

bcall pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit 1ba43047641e1db238ee15a6fa1a1d8dcd371bc5
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Mon Nov 30 12:45:48 2020 -0600

    Add negative caching tests and fixes. (#7361)
    
    This adds test coverage for the negative caching feature and makes some
    fixes as a result of the test's findings.
    
    (cherry picked from commit 8eb68266167d8f8b3fa3a00ca9f6b7889e8ec101)
---
 doc/admin-guide/files/records.config.en.rst        |   4 +-
 doc/admin-guide/performance/index.en.rst           |   4 +-
 mgmt/RecordsConfig.cc                              |   2 +-
 proxy/http/HttpConfig.cc                           |   2 +-
 proxy/http/HttpSM.cc                               |   9 +-
 proxy/http/HttpTransact.cc                         |  38 ++--
 proxy/http/HttpTransact.h                          |  19 +-
 .../autest-site/verifier_server.test.ext           |   4 +
 tests/gold_tests/cache/negative-caching.test.py    | 163 ++++++++++++++++
 ...negative-caching-300-second-timeout.replay.yaml |  72 +++++++
 .../replay/negative-caching-customized.replay.yaml | 164 ++++++++++++++++
 .../replay/negative-caching-default.replay.yaml    | 206 +++++++++++++++++++++
 .../replay/negative-caching-disabled.replay.yaml   | 201 ++++++++++++++++++++
 .../replay/negative-caching-no-timeout.replay.yaml |  53 ++++++
 .../replay/negative-caching-timeout.replay.yaml    |  84 +++++++++
 15 files changed, 993 insertions(+), 32 deletions(-)

diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst
index bc53bff..88cbfe1 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -1625,11 +1625,9 @@ Negative Response Caching
    ====================== =====================================================
    ``204``                No Content
    ``305``                Use Proxy
-   ``400``                Bad Request
    ``403``                Forbidden
    ``404``                Not Found
    ``414``                URI Too Long
-   ``405``                Method Not Allowed
    ``500``                Internal Server Error
    ``501``                Not Implemented
    ``502``                Bad Gateway
@@ -1647,7 +1645,7 @@ Negative Response Caching
    How long (in seconds) |TS| keeps the negative responses  valid in cache. This value only affects negative
    responses that do NOT have explicit ``Expires:`` or ``Cache-Control:`` lifetimes set by the server.
 
-.. ts:cv:: CONFIG proxy.config.http.negative_caching_list STRING 204 305 403 404 405 414 500 501 502 503 504
+.. ts:cv:: CONFIG proxy.config.http.negative_caching_list STRING 204 305 403 404 414 500 501 502 503 504
    :reloadable:
 
    The HTTP status code for negative caching. Default values are mentioned above. The unwanted status codes can be
diff --git a/doc/admin-guide/performance/index.en.rst b/doc/admin-guide/performance/index.en.rst
index 623cdae..f3bd176 100644
--- a/doc/admin-guide/performance/index.en.rst
+++ b/doc/admin-guide/performance/index.en.rst
@@ -495,7 +495,7 @@ Error responses from origins are consistent and costly
 If error responses are costly for your origin server to generate, you may elect
 to have |TS| cache these responses for a period of time. The default behavior is
 to consider all of these responses to be uncacheable, which will lead to every
-client request to result in an origin request.
+client request resulting in an origin request.
 
 This behavior is controlled by both enabling the feature via
 :ts:cv:`proxy.config.http.negative_caching_enabled` and setting the cache time
@@ -504,7 +504,7 @@ status code for negative caching can be set with :ts:cv:`proxy.config.http.negat
 
     CONFIG proxy.config.http.negative_caching_enabled INT 1
     CONFIG proxy.config.http.negative_caching_lifetime INT 10
-    CONFIG proxy.config.http.negative_caching_list STRING 204 305 403 404 405 414 500 501 502 503 504
+    CONFIG proxy.config.http.negative_caching_list STRING 204 305 403 404 414 500 501 502 503 504
 
 SSL-Specific Options
 ~~~~~~~~~~~~~~~~~~~~
diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc
index b9854f9..7120fa5 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -503,7 +503,7 @@ static const RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.http.negative_caching_lifetime", RECD_INT, "1800", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
-  {RECT_CONFIG, "proxy.config.http.negative_caching_list", RECD_STRING, "204 305 403 404 405 414 500 501 502 503 504", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+  {RECT_CONFIG, "proxy.config.http.negative_caching_list", RECD_STRING, "204 305 403 404 414 500 501 502 503 504", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
 
   //        #########################
diff --git a/proxy/http/HttpConfig.cc b/proxy/http/HttpConfig.cc
index b32e39c..9b55951 100644
--- a/proxy/http/HttpConfig.cc
+++ b/proxy/http/HttpConfig.cc
@@ -1036,7 +1036,7 @@ set_negative_caching_list(const char *name, RecDataT dtype, RecData data, HttpCo
   HttpStatusBitset set;
   // values from proxy.config.http.negative_caching_list
   if (0 == strcasecmp("proxy.config.http.negative_caching_list", name) && RECD_STRING == dtype && data.rec_string) {
-    // parse the list of status code
+    // parse the list of status codes
     ts::TextView status_list(data.rec_string, strlen(data.rec_string));
     auto is_sep{[](char c) { return isspace(c) || ',' == c || ';' == c; }};
     while (!status_list.ltrim_if(is_sep).empty()) {
diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc
index 93e61f8..61bb2c0 100644
--- a/proxy/http/HttpSM.cc
+++ b/proxy/http/HttpSM.cc
@@ -3021,7 +3021,7 @@ HttpSM::tunnel_handler_server(int event, HttpTunnelProducer *p)
       // the reason string being written to the client and a bad CL when reading from cache.
       // I didn't find anywhere this appended reason is being used, so commenting it out.
       /*
-        if (t_state.negative_caching && p->bytes_read == 0) {
+        if (t_state.is_cacheable_and_negative_caching_is_enabled && p->bytes_read == 0) {
         int reason_len;
         const char *reason = t_state.hdr_info.server_response.reason_get(&reason_len);
         if (reason == NULL)
@@ -3077,8 +3077,8 @@ HttpSM::tunnel_handler_server(int event, HttpTunnelProducer *p)
   }
 
   // turn off negative caching in case there are multiple server contacts
-  if (t_state.negative_caching) {
-    t_state.negative_caching = false;
+  if (t_state.is_cacheable_and_negative_caching_is_enabled) {
+    t_state.is_cacheable_and_negative_caching_is_enabled = false;
   }
 
   // If we had a ground fill, check update our status
@@ -6725,7 +6725,8 @@ HttpSM::setup_server_transfer()
 
   nbytes = server_transfer_init(buf, hdr_size);
 
-  if (t_state.negative_caching && t_state.hdr_info.server_response.status_get() == HTTP_STATUS_NO_CONTENT) {
+  if (t_state.is_cacheable_and_negative_caching_is_enabled &&
+      t_state.hdr_info.server_response.status_get() == HTTP_STATUS_NO_CONTENT) {
     int s = sizeof("No Content") - 1;
     buf->write("No Content", s);
     nbytes += s;
diff --git a/proxy/http/HttpTransact.cc b/proxy/http/HttpTransact.cc
index c9a1c47..74f5720 100644
--- a/proxy/http/HttpTransact.cc
+++ b/proxy/http/HttpTransact.cc
@@ -4437,7 +4437,7 @@ HttpTransact::handle_cache_operation_on_forward_server_response(State *s)
     client_response_code = server_response_code;
     base_response        = &s->hdr_info.server_response;
 
-    s->negative_caching = is_negative_caching_appropriate(s) && cacheable;
+    s->is_cacheable_and_negative_caching_is_enabled = cacheable && s->txn_conf->negative_caching_enabled;
 
     // determine the correct cache action given the original cache action,
     // cacheability of server response, and request method
@@ -4472,7 +4472,7 @@ HttpTransact::handle_cache_operation_on_forward_server_response(State *s)
       }
 
     } else if (s->cache_info.action == CACHE_DO_WRITE) {
-      if (!cacheable && !s->negative_caching) {
+      if (!cacheable) {
         s->cache_info.action = CACHE_DO_NO_ACTION;
       } else if (s->method == HTTP_WKSIDX_HEAD) {
         s->cache_info.action = CACHE_DO_NO_ACTION;
@@ -4499,7 +4499,7 @@ HttpTransact::handle_cache_operation_on_forward_server_response(State *s)
     //   before issuing a 304
     if (s->cache_info.action == CACHE_DO_WRITE || s->cache_info.action == CACHE_DO_NO_ACTION ||
         s->cache_info.action == CACHE_DO_REPLACE) {
-      if (s->negative_caching) {
+      if (s->is_cacheable_and_negative_caching_is_enabled) {
         HTTPHdr *resp;
         s->cache_info.object_store.create();
         s->cache_info.object_store.request_set(&s->hdr_info.client_request);
@@ -4535,8 +4535,8 @@ HttpTransact::handle_cache_operation_on_forward_server_response(State *s)
           SET_VIA_STRING(VIA_PROXY_RESULT, VIA_PROXY_SERVER_REVALIDATED);
         }
       }
-    } else if (s->negative_caching) {
-      s->negative_caching = false;
+    } else if (s->is_cacheable_and_negative_caching_is_enabled) {
+      s->is_cacheable_and_negative_caching_is_enabled = false;
     }
 
     break;
@@ -4946,7 +4946,7 @@ HttpTransact::set_headers_for_cache_write(State *s, HTTPInfo *cache_info, HTTPHd
      sites yields no insight. So the assert is removed and we keep the behavior that if the response
      in @a cache_info is already set, we don't override it.
   */
-  if (!s->negative_caching || !cache_info->response_get()->valid()) {
+  if (!s->is_cacheable_and_negative_caching_is_enabled || !cache_info->response_get()->valid()) {
     cache_info->response_set(response);
   }
 
@@ -6353,24 +6353,24 @@ HttpTransact::is_response_cacheable(State *s, HTTPHdr *request, HTTPHdr *respons
     }
   }
 
-  // default cacheability
-  if (!s->txn_conf->negative_caching_enabled) {
-    if ((response_code == HTTP_STATUS_OK) || (response_code == HTTP_STATUS_NOT_MODIFIED) ||
-        (response_code == HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION) || (response_code == HTTP_STATUS_MOVED_PERMANENTLY) ||
-        (response_code == HTTP_STATUS_MULTIPLE_CHOICES) || (response_code == HTTP_STATUS_GONE)) {
-      TxnDebug("http_trans", "[is_response_cacheable] YES by default ");
-      return true;
-    } else {
-      TxnDebug("http_trans", "[is_response_cacheable] NO by default");
-      return false;
-    }
+  if ((response_code == HTTP_STATUS_OK) || (response_code == HTTP_STATUS_NOT_MODIFIED) ||
+      (response_code == HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION) || (response_code == HTTP_STATUS_MOVED_PERMANENTLY) ||
+      (response_code == HTTP_STATUS_MULTIPLE_CHOICES) || (response_code == HTTP_STATUS_GONE)) {
+    TxnDebug("http_trans", "[is_response_cacheable] YES response code seems fine");
+    return true;
   }
+  // Notice that the following are not overridable by negative caching.
   if (response_code == HTTP_STATUS_SEE_OTHER || response_code == HTTP_STATUS_UNAUTHORIZED ||
       response_code == HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED) {
     return false;
   }
-  // let is_negative_caching_approriate decide what to do
-  return true;
+  // The response code does not look appropriate for caching. Check, however,
+  // whether the user has specified it should be cached via negative response
+  // caching configuration.
+  if (is_negative_caching_appropriate(s)) {
+    return true;
+  }
+  return false;
   /* Since we weren't caching response obtained with
      Authorization (the cache control stuff was commented out previously)
      I've moved this check to is_request_cache_lookupable().
diff --git a/proxy/http/HttpTransact.h b/proxy/http/HttpTransact.h
index f9900a2..a5ee5cf 100644
--- a/proxy/http/HttpTransact.h
+++ b/proxy/http/HttpTransact.h
@@ -757,8 +757,23 @@ public:
     bool client_connection_enabled = true;
     bool acl_filtering_performed   = false;
 
-    // for negative caching
-    bool negative_caching = false;
+    /// True if negative caching is enabled and the response is cacheable.
+    ///
+    /// Note carefully that this being true does not necessarily imply that the
+    /// response code was negative. It means that (a) the response was
+    /// cacheable apart from response code considerations, and (b) concerning
+    /// the response code one of the following was true:
+    ///
+    ///   * The response was a negative response code configured cacheable
+    ///   by the user via negative response caching configuration, or ...
+    ///
+    ///   * The response code was an otherwise cacheable positive repsonse
+    ///   value (such as a 200 response, for example).
+    ///
+    /// TODO: We should consider refactoring this variable and its use. For now
+    /// I'm giving it an awkwardly long name to make sure the meaning of it is
+    /// clear in its various contexts.
+    bool is_cacheable_and_negative_caching_is_enabled = false;
     // for authenticated content caching
     CacheAuth_t www_auth_content = CACHE_AUTH_NONE;
 
diff --git a/tests/gold_tests/autest-site/verifier_server.test.ext b/tests/gold_tests/autest-site/verifier_server.test.ext
index 52cc92c..3852e22 100755
--- a/tests/gold_tests/autest-site/verifier_server.test.ext
+++ b/tests/gold_tests/autest-site/verifier_server.test.ext
@@ -51,6 +51,8 @@ def _configure_server(obj, process, name, replay_path, http_ports=None, https_po
     if http_ports is None:
         get_port(process, "http_port")
         http_ports = [process.Variables.http_port]
+    else:
+        process.Variables['http_port'] = http_ports[0]
 
     if len(http_ports) > 0:
         command += "--listen "
@@ -60,6 +62,8 @@ def _configure_server(obj, process, name, replay_path, http_ports=None, https_po
     if https_ports is None:
         get_port(process, "https_port")
         https_ports = [process.Variables.https_port]
+    else:
+        process.Variables['https_port'] = https_ports[0]
 
     if len(https_ports) > 0:
         command += '--listen-https '
diff --git a/tests/gold_tests/cache/negative-caching.test.py b/tests/gold_tests/cache/negative-caching.test.py
new file mode 100644
index 0000000..3152ae5
--- /dev/null
+++ b/tests/gold_tests/cache/negative-caching.test.py
@@ -0,0 +1,163 @@
+'''
+Test negative caching.
+'''
+#  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.
+
+Test.Summary = '''
+Test negative caching.
+'''
+
+#
+# Negative caching disabled.
+#
+ts = Test.MakeATSProcess("ts-disabled")
+replay_file = "replay/negative-caching-disabled.replay.yaml"
+server = Test.MakeVerifierServerProcess("server-disabled", replay_file)
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'http',
+    'proxy.config.http.insert_age_in_response': 0,
+
+    'proxy.config.http.negative_caching_enabled': 0
+})
+ts.Disk.remap_config.AddLine(
+    'map / http://127.0.0.1:{0}'.format(server.Variables.http_port)
+)
+tr = Test.AddTestRun("Verify correct behavior without negative caching enabled.")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(ts)
+tr.AddVerifierClientProcess("client-disabled", replay_file, http_ports=[ts.Variables.port])
+
+#
+# Negative caching enabled with otherwise default configuration.
+#
+ts = Test.MakeATSProcess("ts-default")
+replay_file = "replay/negative-caching-default.replay.yaml"
+server = Test.MakeVerifierServerProcess("server-default", replay_file)
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'http',
+    'proxy.config.http.insert_age_in_response': 0,
+
+    'proxy.config.http.negative_caching_enabled': 1
+})
+ts.Disk.remap_config.AddLine(
+    'map / http://127.0.0.1:{0}'.format(server.Variables.http_port)
+)
+tr = Test.AddTestRun("Verify default negative caching behavior")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(ts)
+tr.AddVerifierClientProcess("client-default", replay_file, http_ports=[ts.Variables.port])
+
+#
+# Customized response caching for negative caching configuration.
+#
+ts = Test.MakeATSProcess("ts-customized")
+replay_file = "replay/negative-caching-customized.replay.yaml"
+server = Test.MakeVerifierServerProcess("server-customized", replay_file)
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'http',
+    'proxy.config.http.insert_age_in_response': 0,
+
+    'proxy.config.http.negative_caching_enabled': 1,
+    'proxy.config.http.negative_caching_list': "400"
+})
+ts.Disk.remap_config.AddLine(
+    'map / http://127.0.0.1:{0}'.format(server.Variables.http_port)
+)
+tr = Test.AddTestRun("Verify customized negative caching list")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(ts)
+tr.AddVerifierClientProcess("client-customized", replay_file, http_ports=[ts.Variables.port])
+
+#
+# Verify correct proxy.config.http.negative_caching_lifetime behavior.
+#
+ts = Test.MakeATSProcess("ts-lifetime")
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'http',
+    'proxy.config.http.insert_age_in_response': 0,
+
+    'proxy.config.http.negative_caching_enabled': 1,
+    'proxy.config.http.negative_caching_lifetime': 2
+})
+# This should all behave the same as the default enabled case above.
+tr = Test.AddTestRun("Add a 404 response to the cache")
+replay_file = "replay/negative-caching-default.replay.yaml"
+server = tr.AddVerifierServerProcess("server-lifetime-no-cc", replay_file)
+# Use the same port across the two servers so that the remap config will work
+# across both.
+server_port = server.Variables.http_port
+tr.AddVerifierClientProcess("client-lifetime-no-cc", replay_file, http_ports=[ts.Variables.port])
+ts.Disk.remap_config.AddLine(
+    'map / http://127.0.0.1:{0}'.format(server_port)
+)
+tr.Processes.Default.StartBefore(ts)
+tr.StillRunningAfter = ts
+
+# Wait enough time that the item should be aged out of the cache.
+tr = Test.AddTestRun("Wait for cached object to be stale.")
+tr.Processes.Default.Command = "sleep 4"
+tr.StillRunningAfter = ts
+
+# Verify the item is retrieved from the server instead of the cache.
+replay_file = "replay/negative-caching-timeout.replay.yaml"
+tr = Test.AddTestRun("Make sure object is stale")
+tr.AddVerifierServerProcess("server-timeout", replay_file, http_ports=[server_port])
+tr.AddVerifierClientProcess("client-timeout", replay_file, http_ports=[ts.Variables.port])
+tr.StillRunningAfter = ts
+
+#
+# Verify that the server's Cache-Control overrides the
+# proxy.config.http.negative_caching_lifetime.
+#
+ts = Test.MakeATSProcess("ts-lifetime-2")
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'http',
+    'proxy.config.http.insert_age_in_response': 0,
+
+    'proxy.config.http.negative_caching_enabled': 1,
+    'proxy.config.http.negative_caching_lifetime': 2
+})
+tr = Test.AddTestRun("Add a 404 response with explicit max-age=300 to the cache")
+replay_file = "replay/negative-caching-300-second-timeout.replay.yaml"
+server = tr.AddVerifierServerProcess("server-lifetime-cc", replay_file)
+# Use the same port across the two servers so that the remap config will work
+# across both.
+server_port = server.Variables.http_port
+tr.AddVerifierClientProcess("client-lifetime-cc", replay_file, http_ports=[ts.Variables.port])
+ts.Disk.remap_config.AddLine(
+    'map / http://127.0.0.1:{0}'.format(server_port)
+)
+tr.Processes.Default.StartBefore(ts)
+tr.StillRunningAfter = ts
+
+# Wait enough time that the item should be aged out of the cache if
+# proxy.config.http.negative_caching_lifetime is incorrectly used.
+tr = Test.AddTestRun("Wait for cached object to be stale if lifetime is incorrectly used.")
+tr.Processes.Default.Command = "sleep 4"
+tr.StillRunningAfter = ts
+
+# Verify the item is retrieved from the cache instead of going to the origin.
+replay_file = "replay/negative-caching-no-timeout.replay.yaml"
+tr = Test.AddTestRun("Make sure object is fresh")
+tr.AddVerifierServerProcess("server-no-timeout", replay_file, http_ports=[server_port])
+tr.AddVerifierClientProcess("client-no-timeout", replay_file, http_ports=[ts.Variables.port])
+tr.StillRunningAfter = ts
diff --git a/tests/gold_tests/cache/replay/negative-caching-300-second-timeout.replay.yaml b/tests/gold_tests/cache/replay/negative-caching-300-second-timeout.replay.yaml
new file mode 100644
index 0000000..999b8cd
--- /dev/null
+++ b/tests/gold_tests/cache/replay/negative-caching-300-second-timeout.replay.yaml
@@ -0,0 +1,72 @@
+#  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.
+
+#
+# Create a cached response with a max-age of 300 seconds.
+#
+# This replay file assumes that negative caching is configured to result in the
+# caching of 404 responses (as is the case with default negative caching
+# configuration.)
+#
+
+meta:
+  version: "1.0"
+
+  blocks:
+  - request_404_item: &request_404_item
+      client-request:
+        method: "GET"
+        version: "1.1"
+        scheme: "http"
+        url: /path/404_300_second_timeout
+        headers:
+          fields:
+          - [ Host, example.com ]
+
+sessions:
+- transactions:
+
+  - all: { headers: { fields: [[ uuid, 21 ]]}}
+    <<: *request_404_item
+
+    # Populate the cache with a 404 response.
+    server-response:
+      status: 404
+      reason: "Not Found"
+      headers:
+        fields:
+        - [ Content-Length, 32 ]
+        - [ Cache-Control, max-age=300 ]
+
+    proxy-response:
+      status: 404
+
+  - all: { headers: { fields: [[ uuid, 22 ]]}}
+    <<: *request_404_item
+
+    # 404 responses should be cached when negative caching is enabled, so this
+    # should not get to the server.  But if it does, return a 200 so the test
+    # knows that something went wrong.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Length, 0 ]
+
+    # Expect the cached 404 response.
+    proxy-response:
+      status: 404
diff --git a/tests/gold_tests/cache/replay/negative-caching-customized.replay.yaml b/tests/gold_tests/cache/replay/negative-caching-customized.replay.yaml
new file mode 100644
index 0000000..6d51bd3
--- /dev/null
+++ b/tests/gold_tests/cache/replay/negative-caching-customized.replay.yaml
@@ -0,0 +1,164 @@
+#  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.
+
+#
+# This replay file assumes a negative caching configuration in which 400
+# responses, and only 400 responses, are cached. This is done via
+# `proxy.config.http.negative_caching_list`.
+#
+
+meta:
+  version: "1.0"
+
+  blocks:
+  - 200_response: &200_response
+      server-response:
+        status: 200
+        reason: OK
+        headers:
+          fields:
+          - [ Content-Length, 16 ]
+          - [ Cache-Control, max-age=300 ]
+
+sessions:
+- transactions:
+
+  #
+  # Test 1: Verify that a 404 response is not cached since the custom
+  # negative_caching_list excludes it.
+  #
+  - all: { headers: { fields: [[ uuid, 1 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/404
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    server-response:
+      status: 404
+      reason: "Not Found"
+      headers:
+        fields:
+        - [ Content-Length, 8 ]
+
+    proxy-response:
+      status: 404
+
+  # Request the same item again. It should not be cached and the request should
+  # be forwarded to the server.
+  - all: { headers: { fields: [[ uuid, 2 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/404
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    # Since 404 responses are customized to not be cached, this will go
+    # through.
+    <<: *200_response
+
+    # Expect the server's 200 response.
+    proxy-response:
+      status: 200
+
+  #
+  # Test 2: Verify that a 400 response is cached since the custom
+  # negative_caching_list includes it.
+  #
+  - all: { headers: { fields: [[ uuid, 3 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/400
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    server-response:
+      status: 400
+      reason: "Bad Request"
+      headers:
+        fields:
+        - [ Content-Length, 8 ]
+
+    proxy-response:
+      status: 400
+
+  # Repeat the request and verify the response comes from the cache.
+  - all: { headers: { fields: [[ uuid, 4 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/400
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    # By customization, the 400 will be cached and this will not go through.
+    <<: *200_response
+
+    # Expect the cached 400 response.
+    proxy-response:
+      status: 400
+
+  #
+  # Test 3: Verify that a 200 response is cached since it is a non-negative
+  # response.
+  #
+  - all: { headers: { fields: [[ uuid, 5 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/200
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    <<: *200_response
+
+    proxy-response:
+      status: 200
+
+  - all: { headers: { fields: [[ uuid, 6 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/200
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    # This should not go to the server since the 200 response is cached.
+    server-response:
+      status: 400
+      reason: "Bad Request"
+      headers:
+        fields:
+        - [ Content-Length, 8 ]
+
+    # Expect the cached 200 response.
+    proxy-response:
+      status: 200
diff --git a/tests/gold_tests/cache/replay/negative-caching-default.replay.yaml b/tests/gold_tests/cache/replay/negative-caching-default.replay.yaml
new file mode 100644
index 0000000..f06f30c
--- /dev/null
+++ b/tests/gold_tests/cache/replay/negative-caching-default.replay.yaml
@@ -0,0 +1,206 @@
+#  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.
+
+#
+# This replay file assumes a configuration with negative caching enabled with
+# otherwise default conciguration.
+#
+
+meta:
+  version: "1.0"
+
+  blocks:
+  - 200_response: &200_response
+      server-response:
+        status: 200
+        reason: OK
+        headers:
+          fields:
+          - [ Content-Length, 16 ]
+          - [ Cache-Control, max-age=300 ]
+
+sessions:
+- transactions:
+
+  #
+  # Test 1: Verify that a 404 response is cached.
+  #
+  - all: { headers: { fields: [[ uuid, 1 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/404
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    server-response:
+      status: 404
+      reason: "Not Found"
+      headers:
+        fields:
+        - [ Content-Length, 8 ]
+
+    proxy-response:
+      status: 404
+
+  # Repeat the request and verify that it is served from the cache.
+  - all: { headers: { fields: [[ uuid, 2 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/404
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    # 404 responses should be cached when negative caching is enabled, so this
+    # should not get to the server.  But if it does, return a 200 so the test
+    # knows that something went wrong.
+    <<: *200_response
+
+    # Verify the cached 404 response is served.
+    proxy-response:
+      status: 404
+
+  #
+  # Test 2: Verify that a 400 response is not cached.
+  #
+  - all: { headers: { fields: [[ uuid, 3 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/400
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    server-response:
+      status: 400
+      reason: "Bad Request"
+      headers:
+        fields:
+        - [ Content-Length, 8 ]
+
+    proxy-response:
+      status: 400
+
+  # Repeat the request and verify that the request is forwarded to the server,
+  # not replied with any incorrectly cached response.
+  - all: { headers: { fields: [[ uuid, 4 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/400
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    # 400 responses should not be cached. Verify this goes to the server
+    # by returning and expecting a 200 response.
+    <<: *200_response
+
+    # Verify that the origin's 200 response is served back.
+    proxy-response:
+      status: 200
+
+  #
+  # Test 3: Verify that a 200 response is cached.
+  #
+  - all: { headers: { fields: [[ uuid, 5 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/200
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    <<: *200_response
+
+    proxy-response:
+      status: 200
+
+  - all: { headers: { fields: [[ uuid, 6 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/200
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    # This should not go to the server, but if it does return a 400 so we catch
+    # it.
+    server-response:
+      status: 400
+      reason: "Bad Request"
+      headers:
+        fields:
+        - [ Content-Length, 8 ]
+
+    # Verify that the cached 200 response is served.
+    proxy-response:
+      status: 200
+
+  #
+  # Test 4: Verify that a 405 response is not cached.
+  #
+  - all: { headers: { fields: [[ uuid, 7 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/405
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    server-response:
+      status: 405
+      reason: "Method Not Allowed"
+      headers:
+        fields:
+        - [ Content-Length, 8 ]
+
+    proxy-response:
+      status: 405
+
+  # Repeat the request and verify that the request is forwarded to the server,
+  # not replied with any incorrectly cached response.
+  - all: { headers: { fields: [[ uuid, 8 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/405
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    # 405 responses should not be cached. Verify this goes to the server
+    # by returning and expecting a 200 response.
+    <<: *200_response
+
+    # Verify that the origin's 200 response is served back.
+    proxy-response:
+      status: 200
diff --git a/tests/gold_tests/cache/replay/negative-caching-disabled.replay.yaml b/tests/gold_tests/cache/replay/negative-caching-disabled.replay.yaml
new file mode 100644
index 0000000..1ef338a
--- /dev/null
+++ b/tests/gold_tests/cache/replay/negative-caching-disabled.replay.yaml
@@ -0,0 +1,201 @@
+#  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.
+
+#
+# This replay file assumes a configuration without negative caching enabled.
+#
+
+meta:
+  version: "1.0"
+
+  blocks:
+  - request_for_path_200: &request_for_path_200
+      client-request:
+        method: "GET"
+        version: "1.1"
+        scheme: "http"
+        url: /path/200
+        headers:
+          fields:
+          - [ Host, example.com ]
+
+  - request_for_path_404: &request_for_path_404
+      client-request:
+        method: "GET"
+        version: "1.1"
+        scheme: "http"
+        url: /path/404
+        headers:
+          fields:
+          - [ Host, example.com ]
+
+  - request_for_no_cache_control_response: &request_for_no_cache_control_response
+      client-request:
+        method: "GET"
+        version: "1.1"
+        scheme: "http"
+        url: /path/no_cache_control
+        headers:
+          fields:
+          - [ Host, example.com ]
+
+  - request_for_404_with_cc: &request_for_404_with_cc
+      client-request:
+        method: "GET"
+        version: "1.1"
+        scheme: "http"
+        url: /path/404_with_cc
+        headers:
+          fields:
+          - [ Host, example.com ]
+
+sessions:
+- transactions:
+
+  #
+  # Test 1: Verify that a 200 response is cached.
+  #
+  - all: { headers: { fields: [[ uuid, 1 ]]}}
+    <<: *request_for_path_200
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Length, 16 ]
+        - [ Cache-Control, max-age=300 ]
+
+    proxy-response:
+      status: 200
+
+  - all: { headers: { fields: [[ uuid, 2 ]]}}
+    <<: *request_for_path_200
+
+    # This should not go through to the server. Return a non-200 response to
+    # verify it is served from cache.
+    server-response:
+      status: 400
+      reason: "Bad Request"
+      headers:
+        fields:
+        - [ Content-Length, 0 ]
+
+    # Expect the cached 200 response.
+    proxy-response:
+      status: 200
+
+  #
+  # Test 2: Verify that a 404 response is not cached.
+  #
+  - all: { headers: { fields: [[ uuid, 3 ]]}}
+    <<: *request_for_path_404
+
+    server-response:
+      status: 404
+      reason: "Not Found"
+      headers:
+        fields:
+        - [ Content-Length, 8 ]
+
+    proxy-response:
+      status: 404
+
+  - all: { headers: { fields: [[ uuid, 4 ]]}}
+    <<: *request_for_path_404
+
+    # 404 responses should not be cached. Verify this goes to the server
+    # by returning and expecting a 200 response.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Length, 16 ]
+        - [ Cache-Control, max-age=300 ]
+
+    # Expect the non-cached, origin server 200 response.
+    proxy-response:
+      status: 200
+
+  #
+  # Test 3: Verify that without Cache-Control, a 200 response is not cached.
+  #
+  - all: { headers: { fields: [[ uuid, 5 ]]}}
+    <<: *request_for_no_cache_control_response
+
+    # Reply without a cache-control header. Should not be cached.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Length, 16 ]
+
+    proxy-response:
+      status: 200
+
+  - all: { headers: { fields: [[ uuid, 6 ]]}}
+    <<: *request_for_no_cache_control_response
+
+    # Expect this to go to the origin server since the previous 200 should not
+    # have been cached.
+    server-response:
+      status: 404
+      reason: "Not Found"
+      headers:
+        fields:
+        - [ Content-Length, 8 ]
+
+    # Since there was no Cache-Control, expect the non-cached, origin server
+    # 404 response.
+    proxy-response:
+      status: 404
+
+  #
+  # Test 4: Verify that a negative response is cached if it has a Cache-Control
+  # header. Such a header indicates that the server thinks this is OK to cache.
+  #
+  - all: { headers: { fields: [[ uuid, 7 ]]}}
+    <<: *request_for_404_with_cc
+
+    # Reply with a negative response containing a Cache-Control header.
+    server-response:
+      status: 404
+      reason: "Not Found"
+      headers:
+        fields:
+        - [ Content-Length, 16 ]
+        - [ Cache-Control, max-age=300 ]
+
+    proxy-response:
+      status: 404
+
+  - all: { headers: { fields: [[ uuid, 8 ]]}}
+    <<: *request_for_404_with_cc
+
+    # This should be served out of cache, therefore the origin server should
+    # not see this.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Length, 16 ]
+
+    # Expect the cached 404 response.
+    proxy-response:
+      status: 404
diff --git a/tests/gold_tests/cache/replay/negative-caching-no-timeout.replay.yaml b/tests/gold_tests/cache/replay/negative-caching-no-timeout.replay.yaml
new file mode 100644
index 0000000..6f15278
--- /dev/null
+++ b/tests/gold_tests/cache/replay/negative-caching-no-timeout.replay.yaml
@@ -0,0 +1,53 @@
+#  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.
+
+#
+# Try a get request on /path/404_300_second_timeout and verify it is still in
+# the cache. This is used to verify that the object was not aged out of the
+# cache.
+#
+
+meta:
+  version: "1.0"
+
+sessions:
+- transactions:
+
+  #
+  # Test 1: Verify that the 404 response is is still valid.
+  #
+  - all: { headers: { fields: [[ uuid, 23 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/404_300_second_timeout
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    # This should not go to the server. Verify we get the cached 404 response
+    # instead of this new 403 response.
+    server-response:
+      status: 403
+      reason: "Forbidden"
+      headers:
+        fields:
+        - [ Content-Length, 8 ]
+
+    # Expect the cached 404 response.
+    proxy-response:
+      status: 404
diff --git a/tests/gold_tests/cache/replay/negative-caching-timeout.replay.yaml b/tests/gold_tests/cache/replay/negative-caching-timeout.replay.yaml
new file mode 100644
index 0000000..02cf1e9
--- /dev/null
+++ b/tests/gold_tests/cache/replay/negative-caching-timeout.replay.yaml
@@ -0,0 +1,84 @@
+#  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.
+
+#
+# Try a get request on /path/404 and verify it is not cached. This is used
+# to verify that the object was aged out of the cache.
+#
+
+meta:
+  version: "1.0"
+
+  blocks:
+  - 403_response: &403_response
+      server-response:
+        status: 403
+        reason: "Forbidden"
+        headers:
+          fields:
+          - [ Content-Length, 8 ]
+
+  - 404_response: &404_response
+      server-response:
+        status: 404
+        reason: "Not Found"
+        headers:
+          fields:
+          - [ Content-Length, 0 ]
+
+sessions:
+- transactions:
+
+  #
+  # Test 1: Verify that a 404 response is no longer in the cache.
+  #
+  - all: { headers: { fields: [[ uuid, 10 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/404
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    # This should go to the server. Verify we get a 403 response instead of the
+    # previously cached 404.
+    <<: *403_response
+
+    # Expect the origin server 403, not the stale, cached 404.
+    proxy-response:
+      status: 403
+
+  # For good measure, verify that the new 403 response is cached.
+  - all: { headers: { fields: [[ uuid, 11 ]]}}
+    client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/404
+      headers:
+        fields:
+        - [ Host, example.com ]
+
+    # 403 responses should be cached when negative caching is enabled, so this
+    # should not get to the server.  But if it does, return a 404 so the test
+    # knows that something went wrong.
+    <<: *404_response
+
+    # Expect the cached 403 response.
+    proxy-response:
+      status: 403