You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by bn...@apache.org on 2024/02/07 16:37:49 UTC

(trafficserver) branch master updated: stale_response: RFC 5861 Cache-Control Directives (#11017)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new dacca3d08e stale_response: RFC 5861 Cache-Control Directives (#11017)
dacca3d08e is described below

commit dacca3d08e0eeac1200d9e3e0337900b2e0bcf79
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Wed Feb 7 10:37:40 2024 -0600

    stale_response: RFC 5861 Cache-Control Directives (#11017)
    
    This adds a new experimental plugin called stale_response.so. It
    implements the stale-while-revalidate and stale-if-error Cache-Control
    directives that are described in RFC 5861.
---
 doc/admin-guide/plugins/index.en.rst               |    5 +
 doc/admin-guide/plugins/stale_response.en.rst      |  118 ++
 plugins/experimental/CMakeLists.txt                |    1 +
 plugins/experimental/stale_response/BodyData.h     |  121 +++
 .../{ => stale_response}/CMakeLists.txt            |   42 +-
 plugins/experimental/stale_response/CacheUpdate.cc |  369 +++++++
 plugins/experimental/stale_response/CacheUpdate.h  |   40 +
 .../experimental/stale_response/DirectiveParser.cc |   77 ++
 .../experimental/stale_response/DirectiveParser.h  |   96 ++
 plugins/experimental/stale_response/MurmurHash3.cc |  334 ++++++
 plugins/experimental/stale_response/MurmurHash3.h  |   34 +
 .../experimental/stale_response/NumberToString.cc  |   74 ++
 .../experimental/stale_response/NumberToString.h   |   61 ++
 .../experimental/stale_response/ServerIntercept.cc |  369 +++++++
 .../experimental/stale_response/ServerIntercept.h  |   30 +
 .../experimental/stale_response/UrlComponents.h    |  265 +++++
 .../experimental/stale_response/stale_response.cc  | 1132 ++++++++++++++++++++
 .../experimental/stale_response/stale_response.h   |  143 +++
 plugins/experimental/stale_response/ts_wrap.h      |   42 +
 .../{ => stale_response/unit_tests}/CMakeLists.txt |   33 +-
 .../stale_response/unit_tests/test_BodyData.cc     |   62 ++
 .../unit_tests/test_DirectiveParser.cc             |  103 ++
 .../stale_response/unit_tests/unit_test_main.cc    |   25 +
 .../stale_response/stale_response.test.py          |  173 +++
 .../stale_response_no_default.replay.yaml          |  175 +++
 .../stale_response_with_defaults.replay.yaml       |  144 +++
 .../stale_response_with_force_sie.replay.yaml      |   82 ++
 .../stale_response_with_force_swr.replay.yaml      |   86 ++
 28 files changed, 4188 insertions(+), 48 deletions(-)

diff --git a/doc/admin-guide/plugins/index.en.rst b/doc/admin-guide/plugins/index.en.rst
index ebfb10cc57..66987a533c 100644
--- a/doc/admin-guide/plugins/index.en.rst
+++ b/doc/admin-guide/plugins/index.en.rst
@@ -189,6 +189,7 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi
    Slice <slice.en>
    SSL Headers <sslheaders.en>
    SSL Session Reuse <ssl_session_reuse.en>
+   Stale Response <stale_response.en>
    STEK Share <stek_share.en>
    System Statistics <system_stats.en>
    Wasm <wasm.en>
@@ -271,6 +272,10 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi
 :doc:`SSL Headers <sslheaders.en>`
    Populate request headers with SSL session information.
 
+:doc:`Stale Response <stale_response.en>`
+    Implements handling of the ``stale-while-revalidate`` and ``stale-if-error``
+    ``Cache-Control`` directive extensions.
+
 :doc:`STEK Share <stek_share.en>`
     Coordinates STEK (Session Ticket Encryption Key) between ATS instances running in a group.
 
diff --git a/doc/admin-guide/plugins/stale_response.en.rst b/doc/admin-guide/plugins/stale_response.en.rst
new file mode 100644
index 0000000000..3e60753304
--- /dev/null
+++ b/doc/admin-guide/plugins/stale_response.en.rst
@@ -0,0 +1,118 @@
+.. 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.
+
+
+.. include:: ../../common.defs
+
+.. _admin-plugins-stale_response:
+
+
+Stale Response Plugin
+*********************
+
+This plugin adds ``stale-while-relavidate`` and ``stale-if-error``
+``Cache-Control`` directive functionality.  The ``stale-while-revalidate``
+directive specifies a number of seconds that an item can be stale and used as a
+cached response while |TS| revalidates the cached item. The ``stale-if-error``
+directive specifies how stale a cached response can be and still be used as a
+response if the origin server replies with a 500, 502, 503, or 504 HTTP status.
+The plugin currently only supports the ``stale-if-error`` directive in
+responses. For more details, see `RFC 5861
+<https://www.rfc-editor.org/rfc/rfc5861>`_ for the specification of these
+directives.
+
+Building and Installation
+*************************
+
+The Stale Response plugin is an experimental plugin. To build it, pass
+``-DBUILD_EXPERIMENTAL_PLUGINS=ON`` to the ``cmake`` command when building |TS|.
+By default, that will build the ``stale_response.so`` plugin and install it in
+the ``libexec/trafficserver`` directory.
+
+Configuration
+*************
+
+The Stale Response plugin supports being used as either a global plugin or as a
+remap plugin. To configure |TS| to use the Stale Response plugin, simply add it
+to either the :file:`plugin.config` to configure it as a global plugin and
+restart traffic-server or add it to the desired remap lines in the
+:file:`remap.config` and reload |TS|. The following configurations are available
+for the plugin:
+
+Default values can be specified for responses that do not contain the directives:
+
+  * ``--stale-while-revalidate-default <time>`` set a default
+    ``stale-while-revalidate`` directive with a value of ``time`` for all
+    responses where the directive is not present.
+  * ``--stale-if-error-default <time>`` set a default
+    ``stale-if-err`` directive with a value of ``time`` for all
+    responses where the directive is not present.
+
+Also, minimum values can be set for responses that do or do not contain the
+directives:
+
+  * ``--force-stale-while-revalidate <time>`` set a minimum
+    ``stale-while-revalidate`` time for all responses.
+  * ``--force-stale-if-error <time>`` set a minimum ``stale-if-error`` time
+    for all responses.
+
+The plugin uses memory to temporarily store responses. By default, this is limited
+to 1 GB, but can be configured with: ``--max-memory-usage <size>``
+
+The plugin by default will only perform one asynchronous request at a time for
+a given URL. This can be changed with the ``--force-parallel-async`` option.
+
+Logging
+*******
+
+The plugin can log information about its behavior with respect to the
+``stale-while-revalidate`` and ``stale-if-error`` directives.
+
+  * ``--log-all`` enable logging of all stale responses for both ``stale-while-revalidate``
+    and ``stale-if-error`` directives.
+  * ``--log-stale-while-revalidate``enable logging of all stale responses due to
+    ``stale-while-revalidate`` directives.
+  * ``--log-stale-if-error`` enable logging of all stale responses due to
+    ``stale-if-error`` directives.
+  * ``--log-filename <name>`` set the Stale Response log to ``<name>.log``. The
+    log will be in the :ts:cv:`proxy.config.log.logfile_dir` directory.
+
+
+Statistics
+**********
+
+  * ``stale_response.swr.hit`` The number of times stale data was served for stale-while-relavidate.
+  * ``stale_response.swr.locked_miss`` The number of times stale data could not be served with stale-while-relavidate because of a lock.
+  * ``stale_response.sie.hit`` The number of times stale data was served for stale-if-error.
+  * ``stale_response.memory.over`` The number of times stale data could not be served because of memory constraints.
+
+
+Example
+*******
+
+To configure the plugin as a global plugin with a default
+``stale-while-revalidate`` and a default ``stale-if-error`` of 30 seconds, add
+the following to :file:`plugin.config`::
+
+    stale_response.so --stale-while-revalidate-default 30 --stale-if-error-default 30
+
+To configure the plugin the same way as a remap config plugin, add the following
+to an appropriate remap entry in :file:`remap.config`::
+
+    map http://example.com http://backend.example.com @plugin=stale_response.so \
+      @pparam=--stale-while-revalidate-default @pparam=30 \
+      @pparam=--stale-if-error-default @pparam=30
diff --git a/plugins/experimental/CMakeLists.txt b/plugins/experimental/CMakeLists.txt
index a01426dc15..c398457002 100644
--- a/plugins/experimental/CMakeLists.txt
+++ b/plugins/experimental/CMakeLists.txt
@@ -35,6 +35,7 @@ add_subdirectory(mp4)
 add_subdirectory(rate_limit)
 add_subdirectory(redo_cache_lookup)
 add_subdirectory(sslheaders)
+add_subdirectory(stale_response)
 add_subdirectory(stream_editor)
 add_subdirectory(system_stats)
 add_subdirectory(tls_bridge)
diff --git a/plugins/experimental/stale_response/BodyData.h b/plugins/experimental/stale_response/BodyData.h
new file mode 100644
index 0000000000..fd62ae8118
--- /dev/null
+++ b/plugins/experimental/stale_response/BodyData.h
@@ -0,0 +1,121 @@
+/** @file
+
+  Manage body data for the plugin.
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include <cassert>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <vector>
+
+#include "ts_wrap.h"
+
+// this determines size of pointer array alloc/reallocs
+constexpr unsigned int c_chunk_add_count = 64;
+
+#define PLUGIN_TAG_BODY "stale_response_body"
+
+EXT_DBG_CTL(PLUGIN_TAG_BODY)
+
+/*-----------------------------------------------------------------------------------------------*/
+// This struct needs to newed not malloced
+struct BodyData {
+  BodyData();
+  ~BodyData() = default;
+
+  bool addChunk(const char *start, int64_t size);
+  size_t
+  getChunkCount() const
+  {
+    return chunk_list.size();
+  }
+  bool getChunk(uint32_t chunk_index, const char **start, int64_t *size) const;
+  bool removeChunk(uint32_t chunk);
+  int64_t
+  getSize() const
+  {
+    return total_size;
+  }
+
+  bool intercept_active = false;
+  bool key_hash_active  = false;
+  uint32_t key_hash     = 0;
+
+private:
+  struct Chunk {
+    Chunk(int64_t size, std::vector<char> &&start) : size(size), start(std::move(start)) {}
+
+    int64_t size = 0;
+    std::vector<char> start;
+  };
+  int64_t total_size = 0;
+  std::vector<Chunk> chunk_list;
+};
+
+/*-----------------------------------------------------------------------------------------------*/
+inline BodyData::BodyData()
+{
+  chunk_list.reserve(c_chunk_add_count);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+inline bool
+BodyData::addChunk(const char *start, int64_t size)
+{
+  assert(start != nullptr && size >= 0);
+  chunk_list.emplace_back(size, std::vector<char>(start, start + size));
+  total_size += size;
+  return true;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+inline bool
+BodyData::getChunk(uint32_t chunk_index, const char **start, int64_t *size) const
+{
+  assert(start != nullptr && size != nullptr);
+  bool bGood = false;
+  if (chunk_index < chunk_list.size()) {
+    bGood  = true;
+    *size  = chunk_list[chunk_index].size;
+    *start = chunk_list[chunk_index].start.data();
+  } else {
+    *size  = 0;
+    *start = nullptr;
+  }
+  return bGood;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+inline bool
+BodyData::removeChunk(uint32_t chunk)
+{
+  bool bGood = false;
+  if (chunk < chunk_list.size() && !chunk_list[chunk].start.empty()) {
+    bGood = true;
+    chunk_list[chunk].start.clear();
+  }
+  return bGood;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/CMakeLists.txt b/plugins/experimental/stale_response/CMakeLists.txt
similarity index 54%
copy from plugins/experimental/CMakeLists.txt
copy to plugins/experimental/stale_response/CMakeLists.txt
index a01426dc15..f3b4425805 100644
--- a/plugins/experimental/CMakeLists.txt
+++ b/plugins/experimental/stale_response/CMakeLists.txt
@@ -15,27 +15,21 @@
 #
 #######################
 
-add_subdirectory(access_control)
-add_subdirectory(block_errors)
-add_subdirectory(cache_fill)
-add_subdirectory(cert_reporting_tool)
-add_subdirectory(cookie_remap)
-add_subdirectory(custom_redirect)
-add_subdirectory(fq_pacing)
-add_subdirectory(geoip_acl)
-add_subdirectory(header_freq)
-add_subdirectory(hook-trace)
-add_subdirectory(http_stats)
-add_subdirectory(icap)
-add_subdirectory(inliner)
-add_subdirectory(memcache)
-add_subdirectory(memory_profile)
-add_subdirectory(money_trace)
-add_subdirectory(mp4)
-add_subdirectory(rate_limit)
-add_subdirectory(redo_cache_lookup)
-add_subdirectory(sslheaders)
-add_subdirectory(stream_editor)
-add_subdirectory(system_stats)
-add_subdirectory(tls_bridge)
-add_subdirectory(url_sig)
+project(stale_response)
+
+add_atsplugin(
+  stale_response
+  DirectiveParser.cc
+  CacheUpdate.cc
+  MurmurHash3.cc
+  NumberToString.cc
+  ServerIntercept.cc
+  stale_response.cc
+)
+
+target_link_libraries(stale_response PRIVATE libswoc::libswoc)
+target_include_directories(stale_response PRIVATE "${libswoc_INCLUDE_DIRS}")
+
+if(BUILD_TESTING)
+  add_subdirectory(unit_tests)
+endif()
diff --git a/plugins/experimental/stale_response/CacheUpdate.cc b/plugins/experimental/stale_response/CacheUpdate.cc
new file mode 100644
index 0000000000..3e1f6363a9
--- /dev/null
+++ b/plugins/experimental/stale_response/CacheUpdate.cc
@@ -0,0 +1,369 @@
+/** @file
+
+ Cache Control Extensions Caching Utilities
+
+  @section license License
+
+  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.
+ */
+
+#include <cstring>
+#include <strings.h>
+#include <string>
+#include <netinet/in.h>
+#include <unistd.h>
+
+#include "stale_response.h"
+#include "BodyData.h"
+#include "CacheUpdate.h"
+#include "UrlComponents.h"
+#include "NumberToString.h"
+
+using namespace std;
+
+// unique url parameter that shoud never leave ATS
+static const char ASYNC_PARM[]  = "swrasync=asyncmrl";
+static const int ASYNC_PARM_LEN = sizeof(ASYNC_PARM) - 1;
+// unique header that should never leave ATS
+const char SERVER_INTERCEPT_HEADER[]  = "X-CCExtensions-Intercept";
+const int SERVER_INTERCEPT_HEADER_LEN = sizeof(SERVER_INTERCEPT_HEADER) - 1;
+
+/*-----------------------------------------------------------------------------------------------*/
+static char *
+convert_mime_hdr_to_string(TSMBuffer bufp, TSMLoc hdr_loc)
+{
+  TSIOBuffer output_buffer;
+  TSIOBufferReader reader;
+  int64_t total_avail;
+
+  TSIOBufferBlock block;
+  const char *block_start;
+  int64_t block_avail;
+
+  char *output_string;
+  int output_len;
+
+  output_buffer = TSIOBufferCreate();
+
+  if (!output_buffer) {
+    TSDebug(PLUGIN_TAG_BAD, "[%s] couldn't allocate IOBuffer", __FUNCTION__);
+  }
+
+  reader = TSIOBufferReaderAlloc(output_buffer);
+
+  /* This will print  just MIMEFields and not
+     the http request line */
+  TSMimeHdrPrint(bufp, hdr_loc, output_buffer);
+
+  /* Find out how the big the complete header is by
+     seeing the total bytes in the buffer.  We need to
+     look at the buffer rather than the first block to
+     see the size of the entire header */
+  total_avail = TSIOBufferReaderAvail(reader);
+
+  /* Allocate the string with an extra byte for the string
+     terminator */
+  output_string = static_cast<char *>(TSmalloc(total_avail + 1));
+  output_len    = 0;
+
+  /* We need to loop over all the buffer blocks to make
+     sure we get the complete header since the header can
+     be in multiple blocks */
+  block = TSIOBufferReaderStart(reader);
+  while (block) {
+    block_start = TSIOBufferBlockReadStart(block, reader, &block_avail);
+
+    /* We'll get a block pointer back even if there is no data
+       left to read so check for this condition and break out of
+       the loop. A block with no data to read means we've exhausted
+       buffer of data since if there was more data on a later
+       block in the chain, this block would have been skipped over */
+    if (block_avail == 0) {
+      break;
+    }
+
+    memcpy(output_string + output_len, block_start, block_avail);
+    output_len += block_avail;
+
+    /* Consume the data so that we get to the next block */
+    TSIOBufferReaderConsume(reader, block_avail);
+
+    /* Get the next block now that we've consumed the
+       data off the last block */
+    block = TSIOBufferReaderStart(reader);
+  }
+
+  /* Terminate the string */
+  output_string[output_len] = '\0';
+  output_len++;
+
+  /* Free up the TSIOBuffer that we used to print out the header */
+  TSIOBufferReaderFree(reader);
+  TSIOBufferDestroy(output_buffer);
+
+  return output_string;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+has_trailing_parameter(TSMBuffer hdr_url_buf, TSMLoc hdr_url_loc)
+{
+  bool bFound = false;
+
+  TSMLoc url_loc;
+  TSHttpHdrUrlGet(hdr_url_buf, hdr_url_loc, &url_loc);
+  // create the new url
+  UrlComponents reqUrl;
+  reqUrl.populate(hdr_url_buf, url_loc);
+  string newQuery = reqUrl.getQuery();
+  string::size_type idx;
+  idx = newQuery.find(ASYNC_PARM);
+  if ((idx != string::npos) && (idx + ASYNC_PARM_LEN) == newQuery.length()) {
+    bFound = true;
+  }
+  TSHandleMLocRelease(hdr_url_buf, hdr_url_loc, url_loc);
+  TSDebug(PLUGIN_TAG, "[%s] %d", __FUNCTION__, bFound);
+  return bFound;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+add_trailing_parameter(TSMBuffer hdr_url_buf, TSMLoc hdr_url_loc)
+{
+  TSMLoc url_loc;
+  TSHttpHdrUrlGet(hdr_url_buf, hdr_url_loc, &url_loc);
+  // create the new url
+  UrlComponents reqUrl;
+  reqUrl.populate(hdr_url_buf, url_loc);
+  string newQuery = reqUrl.getQuery();
+
+  if (newQuery.length()) {
+    newQuery += "&";
+    newQuery += ASYNC_PARM;
+  } else {
+    newQuery += ASYNC_PARM;
+  }
+  reqUrl.setQuery(newQuery);
+  string newUrl;
+  reqUrl.construct(newUrl);
+  // parse ans set the new url
+  const char *start = newUrl.c_str();
+  const char *end   = newUrl.size() + start;
+  TSUrlParse(hdr_url_buf, url_loc, &start, end);
+
+  TSDebug(PLUGIN_TAG, "[%s] [%s]", __FUNCTION__, newQuery.c_str());
+  TSHandleMLocRelease(hdr_url_buf, hdr_url_loc, url_loc);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+strip_trailing_parameter(TSMBuffer hdr_url_buf, TSMLoc hdr_url_loc)
+{
+  bool stripped = false;
+  TSMLoc url_loc;
+  TSHttpHdrUrlGet(hdr_url_buf, hdr_url_loc, &url_loc);
+  // create the new url
+  UrlComponents reqUrl;
+  reqUrl.populate(hdr_url_buf, url_loc);
+  string newQuery = reqUrl.getQuery();
+  string::size_type idx;
+  idx = newQuery.find(ASYNC_PARM);
+  if ((idx != string::npos) && (idx + ASYNC_PARM_LEN) == newQuery.length()) {
+    if (idx > 0) {
+      idx -= 1;
+    }
+    newQuery.erase(idx);
+    stripped = true;
+  }
+  if (stripped) {
+    TSUrlHttpQuerySet(hdr_url_buf, url_loc, newQuery.c_str(), newQuery.size());
+  }
+  TSHandleMLocRelease(hdr_url_buf, hdr_url_loc, url_loc);
+  TSDebug(PLUGIN_TAG, "[%s] stripped=%d [%s]", __FUNCTION__, stripped, newQuery.c_str());
+  return stripped;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+fix_connection_close(StateInfo *state)
+{
+  TSMLoc connection_hdr_loc, connection_hdr_dup_loc;
+  connection_hdr_loc = TSMimeHdrFieldFind(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, TS_MIME_FIELD_CONNECTION,
+                                          TS_MIME_LEN_CONNECTION);
+
+  while (connection_hdr_loc != TS_NULL_MLOC) {
+    TSDebug(PLUGIN_TAG, "[%s] {%u} Found old Connection hdr", __FUNCTION__, state->req_info->key_hash);
+    connection_hdr_dup_loc =
+      TSMimeHdrFieldNextDup(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+    TSMimeHdrFieldRemove(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+    TSMimeHdrFieldDestroy(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+    TSHandleMLocRelease(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+    connection_hdr_loc = connection_hdr_dup_loc;
+  }
+
+  TSDebug(PLUGIN_TAG, "[%s] {%u} Creating Connection:close hdr", __FUNCTION__, state->req_info->key_hash);
+  TSMimeHdrFieldCreateNamed(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, TS_MIME_FIELD_CONNECTION,
+                            TS_MIME_LEN_CONNECTION, &connection_hdr_loc);
+  TSMimeHdrFieldValueStringInsert(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc, -1,
+                                  TS_HTTP_VALUE_CLOSE, TS_HTTP_LEN_CLOSE);
+  TSMimeHdrFieldAppend(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+  TSHandleMLocRelease(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+get_pristine_url(StateInfo *state)
+{
+  TSHttpTxn txnp = state->txnp;
+  TSMBuffer hdr_url_buf;
+  TSMLoc url_loc;
+  // get the pristine url only works after remap state
+  if (TSHttpTxnPristineUrlGet(txnp, &hdr_url_buf, &url_loc) == TS_SUCCESS) {
+    int url_length;
+    char *url           = TSUrlStringGet(hdr_url_buf, url_loc, &url_length);
+    state->pristine_url = TSstrndup(url, url_length);
+    TSfree(url);
+    // release the buffer and loc
+    TSHandleMLocRelease(hdr_url_buf, TS_NULL_MLOC, url_loc);
+    TSDebug(PLUGIN_TAG, "[%s] {%u} pristine=[%s]", __FUNCTION__, state->req_info->key_hash, state->pristine_url);
+  } else {
+    TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} TSHttpTxnPristineUrlGet failed!", __FUNCTION__, state->req_info->key_hash);
+  }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+intercept_get_key(TSMBuffer bufp, TSMLoc hdr_loc, const char *name, int name_len, string &key)
+{
+  bool retval      = false;
+  TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, name, name_len);
+  if (field_loc) {
+    int value_len;
+    const char *value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, 0, &value_len);
+    key.append(value, value_len);
+    retval = true;
+  }
+  TSHandleMLocRelease(bufp, hdr_loc, field_loc);
+  // TSDebug(PLUGIN_TAG, "[%s] key=[%s] found=%d",__FUNCTION__,name,key.c_str(),retval);
+  return retval;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+BodyData *
+intercept_check_request(StateInfo *state)
+{
+  uint32_t newKey      = 0;
+  BodyData *pBodyFound = nullptr;
+  TSHttpTxn txnp       = state->txnp;
+  uint32_t oldKey      = state->req_info->key_hash;
+
+  if (!TSHttpTxnIsInternal(txnp)) {
+    TSDebug(PLUGIN_TAG, "[%s] Skipping external request", __FUNCTION__);
+    return pBodyFound;
+  }
+
+  TSMBuffer bufp;
+  TSMLoc hdr_loc;
+  if (TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc) != TS_SUCCESS) {
+    TSDebug(PLUGIN_TAG_BAD, "[%s] TSHttpTxnClientReqGet failed!", __FUNCTION__);
+    return pBodyFound;
+  }
+
+  bool valid_request = false;
+  int method_len;
+  const char *method = TSHttpHdrMethodGet(bufp, hdr_loc, &method_len);
+  if (!method) {
+    TSDebug(PLUGIN_TAG_BAD, "[%s] TSHttpHdrMethodGet failed!", __FUNCTION__);
+  } else {
+    if ((method_len == TS_HTTP_LEN_GET) && (strncasecmp(method, TS_HTTP_METHOD_GET, TS_HTTP_LEN_GET) == 0)) {
+      valid_request = true;
+    }
+  }
+
+  if (valid_request) {
+    string headerKey;
+    if (intercept_get_key(bufp, hdr_loc, SERVER_INTERCEPT_HEADER, SERVER_INTERCEPT_HEADER_LEN, headerKey)) {
+      base16_decode(reinterpret_cast<unsigned char *>(&newKey), headerKey.c_str(), headerKey.length());
+      pBodyFound = async_check_active(newKey, state->plugin_config);
+      if (pBodyFound) {
+        // header key can be differnt because of ATS port wierdness, so make state the same
+        state->req_info->key_hash = newKey;
+      } else {
+        TSDebug(PLUGIN_TAG_BAD, "[%s] key miss %u this should not happen!", __FUNCTION__, newKey);
+      }
+    }
+  }
+
+  TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
+  TSDebug(PLUGIN_TAG, "[%s] {%u} oldKey=%u pBodyFound=%p", __FUNCTION__, newKey, oldKey, pBodyFound);
+  return pBodyFound;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+intercept_fetch_the_url(StateInfo *state)
+{
+  bool bGood = false;
+
+  if (state->pristine_url == nullptr) {
+    TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} pristine url nullptr should not happen", __FUNCTION__, state->req_info->key_hash);
+    if (!async_remove_active(state->req_info->key_hash, state->plugin_config)) {
+      TSDebug(PLUGIN_TAG_BAD, "[%s] didnt delete async active", __FUNCTION__);
+    }
+    return bGood;
+  }
+
+  // encode the key_hash should alwasy be 8 chars
+  char tmpStr[10];
+  base16_encode(tmpStr, reinterpret_cast<unsigned char const *>(&(state->req_info->key_hash)), 4);
+  // create the request as a string
+  string get_request("");
+  get_request.append(TS_HTTP_METHOD_GET);
+  get_request.append(" ");
+  get_request.append(state->pristine_url);
+  get_request.append(" HTTP/1.1\r\n");
+  get_request.append(SERVER_INTERCEPT_HEADER);
+  get_request.append(": ");
+  get_request.append(tmpStr, 8);
+  get_request.append("\r\n");
+
+  char *allReqHeaders = convert_mime_hdr_to_string(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc);
+  get_request.append(allReqHeaders);
+  TSfree(allReqHeaders);
+  get_request.append("\r\n");
+
+  // TSDebug(PLUGIN_TAG, "[%s] req len %d ",__FUNCTION__,(int)get_request.length());
+  // TSDebug(PLUGIN_TAG, "[%s] reg \r\n|%s|\r\n",__FUNCTION__,get_request.c_str());
+
+  BodyData *pBody = async_check_active(state->req_info->key_hash, state->plugin_config);
+  if (pBody) {
+    // TSDebug(PLUGIN_TAG_BAD, "[%s] sleep 4",__FUNCTION__); sleep(4);
+    // This should be safe outside of locks
+    pBody->intercept_active = true;
+    TSFetchEvent event_ids  = {0, 0, 0};
+    TSFetchUrl(get_request.data(), get_request.size(), state->req_info->client_addr, state->transaction_contp, NO_CALLBACK,
+               event_ids);
+    bGood = true;
+    TSDebug(PLUGIN_TAG, "[%s] {%u} length=%d", __FUNCTION__, state->req_info->key_hash, (int)pBody->getSize());
+  } else {
+    TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} cant find body", __FUNCTION__, state->req_info->key_hash);
+  }
+
+  return bGood;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/CacheUpdate.h b/plugins/experimental/stale_response/CacheUpdate.h
new file mode 100644
index 0000000000..0eb4aa6032
--- /dev/null
+++ b/plugins/experimental/stale_response/CacheUpdate.h
@@ -0,0 +1,40 @@
+/** @file
+
+ Cache Control Extensions Cache Utilities
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include "ts/ts.h"
+
+struct StateInfo;
+struct BodyData;
+
+bool has_trailing_parameter(TSMBuffer buf, TSMLoc http_hdr_loc);
+void add_trailing_parameter(TSMBuffer buf, TSMLoc http_hdr_loc);
+bool strip_trailing_parameter(TSMBuffer buf, TSMLoc http_hdr_loc);
+
+void get_pristine_url(StateInfo *state);
+void fix_connection_close(StateInfo *state);
+BodyData *intercept_check_request(StateInfo *state);
+bool intercept_fetch_the_url(StateInfo *state);
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/DirectiveParser.cc b/plugins/experimental/stale_response/DirectiveParser.cc
new file mode 100644
index 0000000000..25ca7c4a79
--- /dev/null
+++ b/plugins/experimental/stale_response/DirectiveParser.cc
@@ -0,0 +1,77 @@
+/** @file
+
+  Parse Cache-Control directives.
+
+  @section license License
+
+  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.
+ */
+
+#include "DirectiveParser.h"
+
+#include "swoc/TextView.h"
+
+namespace
+{
+
+swoc::TextView MAX_AGE{"max-age"};
+swoc::TextView STALE_WHILE_REVALIDATE{"stale-while-revalidate"};
+swoc::TextView STALE_IF_ERROR{"stale-if-error"};
+
+} // anonymous namespace
+
+DirectiveParser::DirectiveParser(swoc::TextView CacheControlValue)
+{
+  while (CacheControlValue) {
+    swoc::TextView directive{CacheControlValue.take_prefix_if(&isspace)};
+
+    // All the directives we care about have a '=' in them.
+    swoc::TextView name{directive.take_prefix_at('=').trim_if(&isspace)};
+    if (!name) {
+      continue;
+    }
+
+    swoc::TextView value{directive.trim_if(&isspace)};
+    if (!value) {
+      continue;
+    }
+    // Directives are separated by commas, so trim if there is one.
+    value.trim(',');
+
+    if (name == MAX_AGE) {
+      this->_max_age = swoc::svtoi(value);
+    } else if (name == STALE_WHILE_REVALIDATE) {
+      this->_stale_while_revalidate_value = swoc::svtoi(value);
+    } else if (name == STALE_IF_ERROR) {
+      this->_stale_if_error_value = swoc::svtoi(value);
+    }
+  }
+}
+
+void
+DirectiveParser::merge(DirectiveParser const &other)
+{
+  if (other._max_age != -1) {
+    this->_max_age = other._max_age;
+  }
+  if (other._stale_while_revalidate_value != -1) {
+    this->_stale_while_revalidate_value = other._stale_while_revalidate_value;
+  }
+  if (other._stale_if_error_value != -1) {
+    this->_stale_if_error_value = other._stale_if_error_value;
+  }
+}
diff --git a/plugins/experimental/stale_response/DirectiveParser.h b/plugins/experimental/stale_response/DirectiveParser.h
new file mode 100644
index 0000000000..ab7f356cea
--- /dev/null
+++ b/plugins/experimental/stale_response/DirectiveParser.h
@@ -0,0 +1,96 @@
+/** @file
+
+  Parse Cache-Control directives.
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include <ctime>
+#include "swoc/TextView.h"
+
+/** Parses the directives of a Cache-Control HTTP field value. */
+class DirectiveParser
+{
+public:
+  DirectiveParser() = default;
+
+  /** Construct a parser from a Cache-Control value.
+   * @param[in] CacheControlValue The value of a Cache-Control header field.
+   */
+  DirectiveParser(swoc::TextView CacheControlValue);
+
+  /** Merge the directives from another parser into this one.
+   *
+   * If a directive is present in both parsers, the value from the other parser is
+   * used.
+   *
+   * @param[in] other The parser to merge into this one.
+   */
+  void merge(DirectiveParser const &other);
+
+  /** Get the value of the max-age Cache-Control directive.
+   *
+   * @return The value of the max-age Cache-Control directive, or -1 if the
+   * directive was not present.
+   */
+  time_t
+  get_max_age() const
+  {
+    return _max_age;
+  };
+
+  /** Get the value of the stale-while-revalidate Cache-Control directive.
+   *
+   * @return The value of the stale-while-revalidate Cache-Control directive,
+   * or -1 if the directive was not present.
+   */
+  time_t
+  get_stale_while_revalidate() const
+  {
+    return _stale_while_revalidate_value;
+  }
+
+  /** Get the value of the stale-if-error Cache-Control directive.
+   *
+   * @return The value of the stale-if-error Cache-Control directive, or -1 if
+   * the directive was not present.
+   */
+  time_t
+  get_stale_if_error() const
+  {
+    return _stale_if_error_value;
+  };
+
+private:
+  /** The value of the max-age Cache-Control directive.
+   *
+   * A negative value indicates that the directive was not present. RFC 9111
+   * section 1.2.2 specifies that delta-seconds, used by max-age, must be
+   * non-negative, so this should be safe.
+   */
+  time_t _max_age = -1;
+
+  /// The value of the stale-while-revalidate Cache-Control directive.
+  time_t _stale_while_revalidate_value = -1;
+
+  /// The value of the stale-if-error Cache-Control directive.
+  time_t _stale_if_error_value = -1;
+};
diff --git a/plugins/experimental/stale_response/MurmurHash3.cc b/plugins/experimental/stale_response/MurmurHash3.cc
new file mode 100644
index 0000000000..600faeb502
--- /dev/null
+++ b/plugins/experimental/stale_response/MurmurHash3.cc
@@ -0,0 +1,334 @@
+/** @file
+
+  Copy of MurmurHash3 implementation.
+
+  @section license License
+
+  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.
+ */
+
+//-----------------------------------------------------------------------------
+// MurmurHash3 was written by Austin Appleby, and is placed in the public
+// domain. The author hereby disclaims copyright to this source code.
+
+// Note - The x86 and x64 versions do _not_ produce the same results, as the
+// algorithms are optimized for their respective platforms. You can still
+// compile and run any of them on any platform, but your performance with the
+// non-native version will be less than optimal.
+
+#include "MurmurHash3.h"
+
+//-----------------------------------------------------------------------------
+#define FORCE_INLINE inline __attribute__((always_inline))
+
+inline uint32_t
+rotl32(uint32_t x, int8_t r)
+{
+  return (x << r) | (x >> (32 - r));
+}
+
+inline uint64_t
+rotl64(uint64_t x, int8_t r)
+{
+  return (x << r) | (x >> (64 - r));
+}
+
+#define ROTL32(x, y) rotl32(x, y)
+#define ROTL64(x, y) rotl64(x, y)
+
+#define BIG_CONSTANT(x) (x##LLU)
+
+//-----------------------------------------------------------------------------
+// Block read - if your platform needs to do endian-swapping or can only
+// handle aligned reads, do the conversion here
+
+FORCE_INLINE uint32_t
+getblock32(const uint32_t *p, int i)
+{
+  return p[i];
+}
+
+FORCE_INLINE uint64_t
+getblock64(const uint64_t *p, int i)
+{
+  return p[i];
+}
+
+//-----------------------------------------------------------------------------
+// Finalization mix - force all bits of a hash block to avalanche
+
+FORCE_INLINE uint32_t
+fmix32(uint32_t h)
+{
+  h ^= h >> 16;
+  h *= 0x85ebca6b;
+  h ^= h >> 13;
+  h *= 0xc2b2ae35;
+  h ^= h >> 16;
+
+  return h;
+}
+
+//----------
+
+FORCE_INLINE uint64_t
+fmix64(uint64_t k)
+{
+  k ^= k >> 33;
+  k *= BIG_CONSTANT(0xff51afd7ed558ccd);
+  k ^= k >> 33;
+  k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53);
+  k ^= k >> 33;
+
+  return k;
+}
+
+//-----------------------------------------------------------------------------
+
+void
+MurmurHash3_x86_32(const void *key, int len, uint32_t seed, void *out)
+{
+  const uint8_t *data = (const uint8_t *)key;
+  const int nblocks   = len / 4;
+
+  uint32_t h1 = seed;
+
+  const uint32_t c1 = 0xcc9e2d51;
+  const uint32_t c2 = 0x1b873593;
+
+  //----------
+  // body
+
+  const uint32_t *blocks = (const uint32_t *)(data + nblocks * 4);
+
+  for (int i = -nblocks; i; i++) {
+    uint32_t k1 = getblock32(blocks, i);
+
+    k1 *= c1;
+    k1  = ROTL32(k1, 15);
+    k1 *= c2;
+
+    h1 ^= k1;
+    h1  = ROTL32(h1, 13);
+    h1  = h1 * 5 + 0xe6546b64;
+  }
+
+  //----------
+  // tail
+
+  const uint8_t *tail = (const uint8_t *)(data + nblocks * 4);
+
+  uint32_t k1 = 0;
+
+  switch (len & 3) {
+  case 3:
+    k1 ^= tail[2] << 16;
+    // fallthrough
+  case 2:
+    k1 ^= tail[1] << 8;
+    // fallthrough
+  case 1:
+    k1 ^= tail[0];
+    k1 *= c1;
+    k1  = ROTL32(k1, 15);
+    k1 *= c2;
+    h1 ^= k1;
+  };
+
+  //----------
+  // finalization
+
+  h1 ^= len;
+
+  h1 = fmix32(h1);
+
+  *(uint32_t *)out = h1;
+}
+
+//-----------------------------------------------------------------------------
+
+void
+MurmurHash3_x86_128(const void *key, const int len, uint32_t seed, void *out)
+{
+  const uint8_t *data = (const uint8_t *)key;
+  const int nblocks   = len / 16;
+
+  uint32_t h1 = seed;
+  uint32_t h2 = seed;
+  uint32_t h3 = seed;
+  uint32_t h4 = seed;
+
+  const uint32_t c1 = 0x239b961b;
+  const uint32_t c2 = 0xab0e9789;
+  const uint32_t c3 = 0x38b34ae5;
+  const uint32_t c4 = 0xa1e38b93;
+
+  //----------
+  // body
+
+  const uint32_t *blocks = (const uint32_t *)(data + nblocks * 16);
+
+  for (int i = -nblocks; i; i++) {
+    uint32_t k1 = getblock32(blocks, i * 4 + 0);
+    uint32_t k2 = getblock32(blocks, i * 4 + 1);
+    uint32_t k3 = getblock32(blocks, i * 4 + 2);
+    uint32_t k4 = getblock32(blocks, i * 4 + 3);
+
+    k1 *= c1;
+    k1  = ROTL32(k1, 15);
+    k1 *= c2;
+    h1 ^= k1;
+
+    h1  = ROTL32(h1, 19);
+    h1 += h2;
+    h1  = h1 * 5 + 0x561ccd1b;
+
+    k2 *= c2;
+    k2  = ROTL32(k2, 16);
+    k2 *= c3;
+    h2 ^= k2;
+
+    h2  = ROTL32(h2, 17);
+    h2 += h3;
+    h2  = h2 * 5 + 0x0bcaa747;
+
+    k3 *= c3;
+    k3  = ROTL32(k3, 17);
+    k3 *= c4;
+    h3 ^= k3;
+
+    h3  = ROTL32(h3, 15);
+    h3 += h4;
+    h3  = h3 * 5 + 0x96cd1c35;
+
+    k4 *= c4;
+    k4  = ROTL32(k4, 18);
+    k4 *= c1;
+    h4 ^= k4;
+
+    h4  = ROTL32(h4, 13);
+    h4 += h1;
+    h4  = h4 * 5 + 0x32ac3b17;
+  }
+
+  //----------
+  // tail
+
+  const uint8_t *tail = (const uint8_t *)(data + nblocks * 16);
+
+  uint32_t k1 = 0;
+  uint32_t k2 = 0;
+  uint32_t k3 = 0;
+  uint32_t k4 = 0;
+
+  switch (len & 15) {
+  case 15:
+    k4 ^= tail[14] << 16;
+    // fallthrough
+  case 14:
+    k4 ^= tail[13] << 8;
+    // fallthrough
+  case 13:
+    k4 ^= tail[12] << 0;
+    k4 *= c4;
+    k4  = ROTL32(k4, 18);
+    k4 *= c1;
+    h4 ^= k4;
+    // fallthrough
+  case 12:
+    k3 ^= tail[11] << 24;
+    // fallthrough
+  case 11:
+    k3 ^= tail[10] << 16;
+    // fallthrough
+  case 10:
+    k3 ^= tail[9] << 8;
+    // fallthrough
+  case 9:
+    k3 ^= tail[8] << 0;
+    k3 *= c3;
+    k3  = ROTL32(k3, 17);
+    k3 *= c4;
+    h3 ^= k3;
+    // fallthrough
+  case 8:
+    k2 ^= tail[7] << 24;
+    // fallthrough
+  case 7:
+    k2 ^= tail[6] << 16;
+    // fallthrough
+  case 6:
+    k2 ^= tail[5] << 8;
+    // fallthrough
+  case 5:
+    k2 ^= tail[4] << 0;
+    k2 *= c2;
+    k2  = ROTL32(k2, 16);
+    k2 *= c3;
+    h2 ^= k2;
+    // fallthrough
+  case 4:
+    k1 ^= tail[3] << 24;
+    // fallthrough
+  case 3:
+    k1 ^= tail[2] << 16;
+    // fallthrough
+  case 2:
+    k1 ^= tail[1] << 8;
+    // fallthrough
+  case 1:
+    k1 ^= tail[0] << 0;
+    k1 *= c1;
+    k1  = ROTL32(k1, 15);
+    k1 *= c2;
+    h1 ^= k1;
+  };
+
+  //----------
+  // finalization
+
+  h1 ^= len;
+  h2 ^= len;
+  h3 ^= len;
+  h4 ^= len;
+
+  h1 += h2;
+  h1 += h3;
+  h1 += h4;
+  h2 += h1;
+  h3 += h1;
+  h4 += h1;
+
+  h1 = fmix32(h1);
+  h2 = fmix32(h2);
+  h3 = fmix32(h3);
+  h4 = fmix32(h4);
+
+  h1 += h2;
+  h1 += h3;
+  h1 += h4;
+  h2 += h1;
+  h3 += h1;
+  h4 += h1;
+
+  ((uint32_t *)out)[0] = h1;
+  ((uint32_t *)out)[1] = h2;
+  ((uint32_t *)out)[2] = h3;
+  ((uint32_t *)out)[3] = h4;
+}
+
+//-----------------------------------------------------------------------------
diff --git a/plugins/experimental/stale_response/MurmurHash3.h b/plugins/experimental/stale_response/MurmurHash3.h
new file mode 100644
index 0000000000..7026d8bead
--- /dev/null
+++ b/plugins/experimental/stale_response/MurmurHash3.h
@@ -0,0 +1,34 @@
+/** @file
+
+  Copy of MurmurHash3 declarations.
+
+  @section license License
+
+  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.
+ */
+
+//-----------------------------------------------------------------------------
+// MurmurHash3 was written by Austin Appleby, and is placed in the public
+// domain. The author hereby disclaims copyright to this source code.
+
+#pragma once
+
+#include <cstdint>
+
+void MurmurHash3_x86_32(const void *key, int len, uint32_t seed, void *out);
+
+void MurmurHash3_x86_128(const void *key, int len, uint32_t seed, void *out);
diff --git a/plugins/experimental/stale_response/NumberToString.cc b/plugins/experimental/stale_response/NumberToString.cc
new file mode 100644
index 0000000000..95ac1a7a5a
--- /dev/null
+++ b/plugins/experimental/stale_response/NumberToString.cc
@@ -0,0 +1,74 @@
+/** @file
+
+  A brief file description
+
+  @section license License
+
+  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.
+ */
+
+#include "NumberToString.h"
+#include <cstring>
+
+/*-----------------------------------------------------------------------------------------------*/
+int
+base16_digit(char ch)
+{
+  if ('0' <= ch && ch <= '9') {
+    return ch - '0';
+  } else if ('A' <= ch && ch <= 'F') {
+    return ch - 'A' + 10;
+  } else if ('a' <= ch && ch <= 'f') {
+    return ch - 'a' + 10;
+  } else {
+    return -1;
+  }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+char *
+base16_encode(char *dst, unsigned char const *src, size_t len)
+{
+  char const hex[] = "0123456789abcdef";
+
+  size_t i;
+  for (i = 0; i != len; ++i) {
+    dst[i * 2 + 0] = hex[src[i] / 16];
+    dst[i * 2 + 1] = hex[src[i] % 16];
+  }
+  dst[i * 2 + 0] = '\0';
+
+  return dst;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+unsigned char *
+base16_decode(unsigned char *dst, char const *src, size_t len)
+{
+  unsigned char *p = dst;
+  for (size_t i = 0; i + 1 < len; i += 2) {
+    int msn = base16_digit(src[i + 0]);
+    int lsn = base16_digit(src[i + 1]);
+    if (msn < 0 || lsn < 0) {
+      break;
+    }
+    *p++ = static_cast<unsigned char>((msn << 4) | lsn);
+  }
+  return dst;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/NumberToString.h b/plugins/experimental/stale_response/NumberToString.h
new file mode 100644
index 0000000000..7fd047221b
--- /dev/null
+++ b/plugins/experimental/stale_response/NumberToString.h
@@ -0,0 +1,61 @@
+/** @file
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include <cstddef>
+
+/**
+ *  Convert the base-16 digit <TT>ch</TT> into an integer.  If the
+ *  digit is invalid then <TT>-1</TT> is returned.
+ *
+ *  @param ch           the base-16 digit to convert.
+ *  @return             an integer in the range [0, 15] or -1 if the
+ *                      character is NOT a base-16 digit.
+ **/
+int base16_digit(char ch);
+
+/**
+ *  Convert <TT>len</TT> bytes of binary data in <TT>src</TT> into
+ *  <TT>2 * len + 1</TT> hexadecimal digits stored in <TT>dst</TT>.
+ *
+ *  @param dst          a buffer of length <TT>2 * len + 1</TT>.
+ *  @param src          the source binary data to convert.
+ *  @param len          the number of bytes of binary data in src
+ *                      to convert into hexadecimal.
+ *  @return             the dst pointer.
+ **/
+char *base16_encode(char *dst, unsigned char const *src, size_t len);
+
+/**
+ *  Convert <TT>len</TT> hexadecimal digits in <TT>src</TT> into
+ *  <TT>len / 2</TT> bytes of binary data stored in <TT>dst</TT>.
+ *
+ *  @param dst          a buffer of length <TT>len / 2</TT>.
+ *  @param src          the source hexadecimal digits to convert.
+ *  @param len          the number of bytes hexadecimal digits in
+ *                      src to convert into binary data.  If this
+ *                      value is odd then the last digit is ignored.
+ *  @return             the dst pointer.
+ **/
+unsigned char *base16_decode(unsigned char *dst, char const *src, size_t len);
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/ServerIntercept.cc b/plugins/experimental/stale_response/ServerIntercept.cc
new file mode 100644
index 0000000000..50837819c5
--- /dev/null
+++ b/plugins/experimental/stale_response/ServerIntercept.cc
@@ -0,0 +1,369 @@
+/** @file
+
+  @section license License
+
+  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.
+ */
+
+#include "ServerIntercept.h"
+#include "BodyData.h"
+#include "stale_response.h"
+
+#include "ts/ts.h"
+#include "ts_wrap.h"
+
+#include <climits>
+#include <cstdio>
+#include <cstring>
+#include <search.h>
+#include <strings.h>
+
+#include <arpa/inet.h>
+
+using namespace std;
+
+#define PLUGIN_TAG_SERV "stale_response_intercept"
+const int64_t c_max_single_write = 64 * 1024;
+
+DEF_DBG_CTL(PLUGIN_TAG_SERV)
+
+/*-----------------------------------------------------------------------------------------------*/
+struct SContData {
+  struct IoHandle {
+    TSVIO vio;
+    TSIOBuffer buffer;
+    TSIOBufferReader reader;
+    IoHandle() : vio(0), buffer(0), reader(0){};
+    ~IoHandle()
+    {
+      if (reader) {
+        TSIOBufferReaderFree(reader);
+      }
+      if (buffer) {
+        TSIOBufferDestroy(buffer);
+      }
+    };
+  };
+
+  TSVConn net_vc;
+  TSCont contp;
+
+  IoHandle input;
+  IoHandle output;
+  TSHttpParser http_parser;
+  TSMBuffer req_hdr_bufp;
+  TSMLoc req_hdr_loc;
+  bool req_hdr_parsed;
+
+  bool conn_setup;
+  bool write_setup;
+
+  ConfigInfo *plugin_config;
+  BodyData *pBody;
+  uint32_t next_chunk_written;
+
+  SContData(TSCont cont)
+    : net_vc(0),
+      contp(cont),
+      input(),
+      output(),
+      req_hdr_bufp(0),
+      req_hdr_loc(0),
+      req_hdr_parsed(false),
+      conn_setup(false),
+      write_setup(false),
+      plugin_config(nullptr),
+      pBody(nullptr),
+      next_chunk_written(0)
+  {
+    http_parser = TSHttpParserCreate();
+  }
+
+  ~SContData()
+  {
+    TSDebug(PLUGIN_TAG_SERV, "[%s] Destroying continuation data", __FUNCTION__);
+    TSHttpParserDestroy(http_parser);
+    if (req_hdr_loc) {
+      TSHandleMLocRelease(req_hdr_bufp, TS_NULL_MLOC, req_hdr_loc);
+    }
+    if (req_hdr_bufp) {
+      TSMBufferDestroy(req_hdr_bufp);
+    }
+  };
+};
+
+/*-----------------------------------------------------------------------------------------------*/
+static bool
+connSetup(SContData *cont_data, TSVConn vconn)
+{
+  if (cont_data->conn_setup) {
+    TSDebug(PLUGIN_TAG_BAD, "[%s] SContData already init", __FUNCTION__);
+    return false;
+  }
+  cont_data->conn_setup = true;
+  cont_data->net_vc     = vconn;
+
+  cont_data->input.buffer = TSIOBufferCreate();
+  cont_data->input.reader = TSIOBufferReaderAlloc(cont_data->input.buffer);
+  cont_data->input.vio    = TSVConnRead(cont_data->net_vc, cont_data->contp, cont_data->input.buffer, INT_MAX);
+
+  cont_data->req_hdr_bufp = TSMBufferCreate();
+  cont_data->req_hdr_loc  = TSHttpHdrCreate(cont_data->req_hdr_bufp);
+  TSHttpHdrTypeSet(cont_data->req_hdr_bufp, cont_data->req_hdr_loc, TS_HTTP_TYPE_REQUEST);
+
+  TSDebug(PLUGIN_TAG_SERV, "[%s] Done", __FUNCTION__);
+  return true;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+connShutdownDataDestory(SContData *cont_data)
+{
+  if (cont_data->net_vc) {
+    TSVConnClose(cont_data->net_vc);
+  }
+  // we need to destroy the body data object
+  if (cont_data->pBody->key_hash_active) {
+    if (!async_remove_active(cont_data->pBody->key_hash, cont_data->plugin_config)) {
+      TSDebug(PLUGIN_TAG_BAD, "[%s] didnt delete async active", __FUNCTION__);
+    }
+  } else {
+    delete cont_data->pBody;
+  }
+  // clean up my cont
+  TSContDestroy(cont_data->contp);
+  delete cont_data;
+  TSDebug(PLUGIN_TAG_SERV, "[%s] Done", __FUNCTION__);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static bool
+writeOutData(SContData *cont_data)
+{
+  int64_t total_current_write = 0;
+  uint32_t max_chunk_count    = cont_data->pBody->getChunkCount();
+  for (uint32_t chunk_index = cont_data->next_chunk_written; chunk_index < max_chunk_count; chunk_index++) {
+    const char *start;
+    int64_t avail;
+    if (!cont_data->pBody->getChunk(chunk_index, &start, &avail)) {
+      TSDebug(PLUGIN_TAG_BAD, "[%s] Error while getting chunk_index %d", __FUNCTION__, chunk_index);
+      TSError("[%s] Error while getting chunk_index %d", __FUNCTION__, chunk_index);
+      break;
+    }
+    if (TSIOBufferWrite(cont_data->output.buffer, start, avail) != avail) {
+      TSDebug(PLUGIN_TAG_BAD, "[%s] Error while writing content avail=%d", __FUNCTION__, (int)avail);
+    }
+    cont_data->pBody->removeChunk(chunk_index);
+    total_current_write           += avail;
+    cont_data->next_chunk_written  = chunk_index + 1;
+    if (total_current_write >= c_max_single_write) {
+      break;
+    }
+  }
+  TSVIOReenable(cont_data->output.vio);
+
+  // TSDebug(PLUGIN_TAG_SERV, "[%s] written=%d done=%d", __FUNCTION__, (int)total_current_write,(cont_data->next_chunk_written >=
+  // max_chunk_count));
+  return (cont_data->next_chunk_written >= max_chunk_count);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+writeSetup(SContData *cont_data)
+{
+  if (!cont_data->write_setup) {
+    cont_data->write_setup   = true;
+    cont_data->output.buffer = TSIOBufferCreate();
+    cont_data->output.reader = TSIOBufferReaderAlloc(cont_data->output.buffer);
+    cont_data->output.vio    = TSVConnWrite(cont_data->net_vc, cont_data->contp, cont_data->output.reader, INT_MAX);
+    // set the total length to write
+    TSVIONBytesSet(cont_data->output.vio, cont_data->pBody->getSize());
+    TSDebug(PLUGIN_TAG_SERV, "[%s] Done length=%d", __FUNCTION__, (int)cont_data->pBody->getSize());
+  } else {
+    TSDebug(PLUGIN_TAG_BAD, "[%s] Already init", __FUNCTION__);
+  }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static bool
+handleRead(SContData *cont_data)
+{
+  int avail = TSIOBufferReaderAvail(cont_data->input.reader);
+  if (avail == TS_ERROR) {
+    TSError("[%s] Error while getting number of bytes available", __FUNCTION__);
+    return false;
+  }
+
+  TSDebug(PLUGIN_TAG_SERV, "[%s] avail %d", __FUNCTION__, avail);
+
+  int consumed = 0;
+  if (avail > 0) {
+    int64_t data_len;
+    const char *data;
+    TSIOBufferBlock block = TSIOBufferReaderStart(cont_data->input.reader);
+    while (block != nullptr) {
+      data = TSIOBufferBlockReadStart(block, cont_data->input.reader, &data_len);
+
+      if (!cont_data->req_hdr_parsed) {
+        const char *endptr = data + data_len;
+        if (TSHttpHdrParseReq(cont_data->http_parser, cont_data->req_hdr_bufp, cont_data->req_hdr_loc, &data, endptr) ==
+            TS_PARSE_DONE) {
+          cont_data->req_hdr_parsed = true;
+          TSDebug(PLUGIN_TAG_SERV, "[%s] Parsed header", __FUNCTION__);
+        }
+      }
+      consumed += data_len;
+      block     = TSIOBufferBlockNext(block);
+    }
+  }
+
+  TSIOBufferReaderConsume(cont_data->input.reader, consumed);
+
+  TSDebug(PLUGIN_TAG_SERV, "[%s] Consumed %d bytes from input vio, avail: %d", __FUNCTION__, consumed, avail);
+
+  // Modify the input VIO to reflect how much data we've completed.
+  TSVIONDoneSet(cont_data->input.vio, TSVIONDoneGet(cont_data->input.vio) + consumed);
+
+  if (!cont_data->req_hdr_parsed) {
+    TSDebug(PLUGIN_TAG_SERV, "[%s] Reenabling input vio need more header data", __FUNCTION__);
+    TSVIOReenable(cont_data->input.vio);
+  }
+
+  return true;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static int
+serverIntercept(TSCont contp, TSEvent event, void *edata)
+{
+  SContData *cont_data = static_cast<SContData *>(TSContDataGet(contp));
+  bool shutdown        = false;
+
+  switch (event) {
+  case TS_EVENT_NET_ACCEPT:
+    TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} net accept event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+    if (!connSetup(cont_data, static_cast<TSVConn>(edata))) {
+      TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} connSetup aleady initalized", __FUNCTION__, cont_data->pBody->key_hash);
+    }
+    break;
+
+  case TS_EVENT_NET_ACCEPT_FAILED:
+    // not sure why this would happen, but it does
+    TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} net accept failed %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+    shutdown = true;
+    break;
+
+  case TS_EVENT_VCONN_READ_READY:
+    TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} vconn read ready event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+    if (!handleRead(cont_data)) {
+      TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} handleRead failed", __FUNCTION__, cont_data->pBody->key_hash);
+      break;
+    }
+    // VCONN_READ_READY should not happen again since we dont reenable input.vio
+    if (cont_data->req_hdr_parsed && !cont_data->write_setup) {
+      writeSetup(cont_data);
+      writeOutData(cont_data);
+    }
+    break;
+
+  case TS_EVENT_VCONN_READ_COMPLETE:
+  case TS_EVENT_VCONN_EOS:
+    // intentional fall-through
+    TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} vconn read complete/eos event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+    // shutdown id we havent parsed headers and get a read eos/complete
+    if (!cont_data->req_hdr_parsed) {
+      TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} read complete but headers not parsed", __FUNCTION__, cont_data->pBody->key_hash);
+      shutdown = true;
+    }
+    break;
+
+  case TS_EVENT_VCONN_WRITE_READY:
+    // TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} vconn write ready event %d", __FUNCTION__,cont_data->pBody->key_hash,event);
+    // trying not to write out the whole body at once if its big
+    writeOutData(cont_data);
+    break;
+
+  case TS_EVENT_VCONN_WRITE_COMPLETE:
+    TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} vconn write complete event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+    shutdown = true;
+    break;
+
+  case TS_EVENT_ERROR:
+    // todo: do some error handling here
+    TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} error event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+    shutdown = true;
+    break;
+
+  default:
+    TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} default event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+    break;
+  }
+
+  if (shutdown) {
+    connShutdownDataDestory(cont_data);
+  }
+
+  return 1;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+serverInterceptSetup(TSHttpTxn txnp, BodyData *pBody, ConfigInfo *plugin_config)
+{
+  // make sure we have data to deliver -- note called body but its headers+body
+  if (!pBody || (pBody->getSize() <= 0)) {
+    TSDebug(PLUGIN_TAG_BAD, "[%s] must have body and size > 0", __FUNCTION__);
+    // remove async active if pBody exists but size <= 0
+    if (pBody && pBody->key_hash_active) {
+      if (async_remove_active(pBody->key_hash, plugin_config)) {
+        TSDebug(PLUGIN_TAG_BAD, "[%s] removed async active due to pbody size <= 0", __FUNCTION__);
+      } else {
+        TSDebug(PLUGIN_TAG_BAD, "[%s] failed to delete async active when pbody size <= 0", __FUNCTION__);
+      }
+    }
+    return false;
+  }
+  // make sure we have a contp
+  TSCont contp = TSContCreate(serverIntercept, TSMutexCreate());
+  if (!contp) {
+    TSDebug(PLUGIN_TAG_BAD, "[%s] Could not create intercept contp", __FUNCTION__);
+    // remove async active if couldn't create intercept contp
+    if (pBody->key_hash_active) {
+      if (async_remove_active(pBody->key_hash, plugin_config)) {
+        TSDebug(PLUGIN_TAG_BAD, "[%s] removed async active since couldn't create intercept contp", __FUNCTION__);
+      } else {
+        TSDebug(PLUGIN_TAG_BAD, "[%s] failed to delete async active when couldn't create intercept contp", __FUNCTION__);
+      }
+    }
+    return false;
+  }
+  // create the data block for the cont
+  SContData *cont_data = new SContData(contp);
+  // set some pointers
+  cont_data->plugin_config = plugin_config;
+  cont_data->pBody         = pBody;
+  // set the intercept and turn on caching
+  TSContDataSet(contp, cont_data);
+  TSHttpTxnServerIntercept(contp, txnp);
+  TSHttpTxnReqCacheableSet(txnp, 1);
+  TSHttpTxnRespCacheableSet(txnp, 1);
+  TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} Success length=%d", __FUNCTION__, cont_data->pBody->key_hash,
+          (int)cont_data->pBody->getSize());
+  return true;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/ServerIntercept.h b/plugins/experimental/stale_response/ServerIntercept.h
new file mode 100644
index 0000000000..47f5d20ea1
--- /dev/null
+++ b/plugins/experimental/stale_response/ServerIntercept.h
@@ -0,0 +1,30 @@
+/** @file
+
+  @section license License
+
+  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.
+ */
+#pragma once
+
+#include "ts/ts.h"
+
+struct BodyData;
+struct ConfigInfo;
+
+bool serverInterceptSetup(TSHttpTxn txnp, BodyData *pBody, ConfigInfo *plugin_config);
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/UrlComponents.h b/plugins/experimental/stale_response/UrlComponents.h
new file mode 100644
index 0000000000..6673b4ba2b
--- /dev/null
+++ b/plugins/experimental/stale_response/UrlComponents.h
@@ -0,0 +1,265 @@
+/** @file
+
+ URL helper
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include "ts/ts.h"
+
+#include <cstdio>
+#include <string>
+
+///////////////////////////////////////////////////////////////////////////////
+// Class holding one request URL's component.
+//
+
+struct UrlComponents {
+  UrlComponents() : _port(0) {}
+
+  void
+  populate(TSMBuffer bufp, TSMLoc urlLoc)
+  {
+    int scheme_len;
+    int host_len;
+    int path_len;
+    int query_len;
+    int matrix_len;
+
+    const char *scheme = TSUrlSchemeGet(bufp, urlLoc, &scheme_len);
+    const char *host   = TSUrlHostGet(bufp, urlLoc, &host_len);
+    const char *path   = TSUrlPathGet(bufp, urlLoc, &path_len);
+    const char *query  = TSUrlHttpQueryGet(bufp, urlLoc, &query_len);
+    const char *matrix = TSUrlHttpParamsGet(bufp, urlLoc, &matrix_len);
+    _port              = TSUrlPortGet(bufp, urlLoc);
+
+    _scheme.assign(scheme, scheme_len);
+    _host.assign(host, host_len);
+    _path.assign(path, path_len);
+    _query.assign(query, query_len);
+    _matrix.assign(matrix, matrix_len);
+  }
+
+  // get entire url (e.g.http://host/path?query)
+  void
+  construct(std::string &url)
+  {
+    // schemeExtra= :// = 3
+    // portExtra = :xxxxx = 6
+    // just in case extra = 32
+    size_t iLen = _scheme.size() + _host.size() + _path.size() + _query.size() + _matrix.size() + 3 + 6 + 32;
+    url.reserve(iLen);
+
+    const int bitAddPort   = 1;
+    const int bitAddQuery  = 1 << 1;
+    const int bitAddMatrix = 1 << 2;
+    int bitField           = bitAddPort; // add port by default
+    if ((_scheme.compare("http") == 0 && _port == 80) || (_scheme.compare("https") == 0 && _port == 443)) {
+      bitField &= ~bitAddPort;
+    }
+    if (_query.size() != 0) {
+      bitField |= bitAddQuery;
+    }
+    if (_matrix.size() != 0) {
+      bitField |= bitAddMatrix;
+    }
+
+    switch (bitField) {
+    case 0: // default port, no query, no matrix
+      url = _scheme + "://" + _host + "/" + _path;
+      break;
+
+    case bitAddPort: { //  port, no query, no matrix
+      char sTemp[PORT_BUFFER_SIZE];
+      url = _scheme + "://" + _host + ":";
+      snprintf(sTemp, PORT_BUFFER_SIZE, "%d", _port);
+      url += sTemp;
+      url += "/" + _path;
+      break;
+    }
+
+    case bitAddQuery: //  default port, with query, no matrix
+      url = _scheme + "://" + _host + "/" + _path + "?" + _query;
+      break;
+    case bitAddQuery | bitAddMatrix: //  default port, with query, with matrix (even possible?)
+      url = _scheme + "://" + _host + "/" + _path + ";" + _matrix + "?" + _query;
+      break;
+
+    case bitAddMatrix: //  default port, no query, with matrix
+      url = _scheme + "://" + _host + "/" + _path + ";" + _matrix;
+      break;
+
+    case bitAddPort | bitAddQuery: //  port, with query, no matrix
+    {                              // port, with query, with matrix (even possible?)
+      char sTemp[PORT_BUFFER_SIZE];
+      url = _scheme + "://" + _host + ":";
+      snprintf(sTemp, PORT_BUFFER_SIZE, "%d", _port);
+      url += sTemp;
+      url += "/" + _path + "?" + _query;
+      break;
+    }
+
+    case bitAddPort | bitAddQuery | bitAddMatrix: { // port, with query, with matrix (even possible?)
+      char sTemp[PORT_BUFFER_SIZE];
+      url = _scheme + "://" + _host + ":";
+      snprintf(sTemp, PORT_BUFFER_SIZE, "%d", _port);
+      url += sTemp;
+      url += "/" + _path + ";" + _matrix + "?" + _query;
+      break;
+    }
+
+    case bitAddPort | bitAddMatrix: { //  port, no query, with matrix
+      char sTemp[PORT_BUFFER_SIZE];
+      url = _scheme + "://" + _host + ":";
+      snprintf(sTemp, PORT_BUFFER_SIZE, "%d", _port);
+      url += sTemp;
+      url += "/" + _path + ";" + _matrix;
+      break;
+    }
+    }
+  }
+
+  // get path w/query or matrix
+  void
+  getCompletePathString(std::string &p)
+  {
+    // schemeExtra= :// = 3
+    // portExtra = :xxxxx = 6
+    // just in case extra = 32
+    size_t iLen = _path.size() + _query.size() + _matrix.size() + 3 + 6 + 32;
+    p.reserve(iLen);
+
+    int bitField           = 0;
+    const int bitAddQuery  = 1 << 1;
+    const int bitAddMatrix = 1 << 2;
+    if (_query.size() != 0) {
+      bitField |= bitAddQuery;
+    }
+    if (_matrix.size() != 0) {
+      bitField |= bitAddMatrix;
+    }
+
+    switch (bitField) {
+    case bitAddQuery: //  default path, with query, no matrix
+      p = "/" + _path + "?" + _query;
+      break;
+    case bitAddQuery | bitAddMatrix: //  default path, with query, with matrix (even possible?)
+      p = "/" + _path + ";" + _matrix + "?" + _query;
+      break;
+
+    case bitAddMatrix: //  default port, no query, with matrix
+      p = "/" + _path + ";" + _matrix;
+      break;
+
+    default:
+    case 0: // default path, no query, no matrix
+      p = "/" + _path;
+      break;
+    }
+  }
+
+  // get string w/port if different than scheme default
+  void
+  getCompleteHostString(std::string &host)
+  {
+    if ((_scheme.compare("http") == 0 && _port == 80) ||
+        (_scheme.compare("https") == 0 && _port == 443)) { // port is default for scheme
+      host = _host;
+    } else { // port is different than scheme default, so include
+      char sTemp[PORT_BUFFER_SIZE];
+      host = _host + ":";
+      snprintf(sTemp, PORT_BUFFER_SIZE, "%d", _port);
+      host += sTemp;
+    }
+  }
+
+  void
+  setScheme(std::string &s)
+  {
+    _scheme = s;
+  };
+  void
+  setHost(std::string &h)
+  {
+    _host = h;
+  };
+  void
+  setPath(std::string &p)
+  {
+    _path = p;
+  };
+  void
+  setQuery(std::string &q)
+  {
+    _query = q;
+  };
+  void
+  setMatrix(std::string &m)
+  {
+    _matrix = m;
+  };
+  void
+  setPoinht(int p)
+  {
+    _port = p;
+  };
+
+  const std::string &
+  getScheme()
+  {
+    return _scheme;
+  };
+  const std::string &
+  getHost()
+  {
+    return _host;
+  };
+  const std::string &
+  getPath()
+  {
+    return _path;
+  };
+  const std::string &
+  getQuery()
+  {
+    return _query;
+  };
+  const std::string &
+  getMatrix()
+  {
+    return _matrix;
+  };
+  int
+  getPort()
+  {
+    return _port;
+  };
+
+private:
+  constexpr static size_t PORT_BUFFER_SIZE = 10;
+
+  std::string _scheme;
+  std::string _host;
+  std::string _path;
+  std::string _query;
+  std::string _matrix;
+  int _port;
+};
diff --git a/plugins/experimental/stale_response/stale_response.cc b/plugins/experimental/stale_response/stale_response.cc
new file mode 100644
index 0000000000..4ef7626879
--- /dev/null
+++ b/plugins/experimental/stale_response/stale_response.cc
@@ -0,0 +1,1132 @@
+/** @file
+
+  Implements RFC 5861 (HTTP Cache-Control Extensions for Stale Content)
+
+  @section license License
+
+  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.
+ */
+
+#include "stale_response.h"
+#include "BodyData.h"
+#include "CacheUpdate.h"
+#include "DirectiveParser.h"
+#include "MurmurHash3.h"
+#include "ServerIntercept.h"
+
+#include "swoc/TextView.h"
+#include "ts/apidefs.h"
+#include "ts/remap.h"
+#include "ts/remap_version.h"
+#include "ts_wrap.h"
+
+#include <arpa/inet.h>
+#include <cinttypes>
+#include <cassert>
+#include <cstdlib>
+#include <cstring>
+#include <ctime>
+#include <getopt.h>
+#include <string>
+
+using namespace std;
+
+const char PLUGIN_TAG[]     = "stale_response";
+const char PLUGIN_TAG_BAD[] = "stale_response_bad";
+
+DEF_DBG_CTL(PLUGIN_TAG)
+DEF_DBG_CTL(PLUGIN_TAG_BAD)
+DEF_DBG_CTL(PLUGIN_TAG_BODY)
+
+static const char VENDOR_NAME[]   = "Apache Software Foundation";
+static const char SUPPORT_EMAIL[] = "dev@trafficserver.apache.org";
+
+static const char HTTP_VALUE_STALE_WARNING[]    = "110 Response is stale";
+static const char SIE_SERVER_INTERCEPT_HEADER[] = "@X-CCExtensions-Sie-Intercept";
+static const char HTTP_VALUE_SERVER_INTERCEPT[] = "1";
+
+/*-----------------------------------------------------------------------------------------------*/
+static ResponseInfo *
+create_response_info(void)
+{
+  ResponseInfo *resp_info = static_cast<ResponseInfo *>(TSmalloc(sizeof(ResponseInfo)));
+
+  resp_info->http_hdr_buf = TSMBufferCreate();
+  resp_info->http_hdr_loc = TSHttpHdrCreate(resp_info->http_hdr_buf);
+  resp_info->parser       = TSHttpParserCreate();
+  resp_info->parsed       = false;
+
+  return resp_info;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+free_response_info(ResponseInfo *resp_info)
+{
+  TSHandleMLocRelease(resp_info->http_hdr_buf, TS_NULL_MLOC, resp_info->http_hdr_loc);
+  TSMBufferDestroy(resp_info->http_hdr_buf);
+  TSHttpParserDestroy(resp_info->parser);
+  TSfree(resp_info);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static RequestInfo *
+create_request_info(TSHttpTxn txnp)
+{
+  RequestInfo *req_info = static_cast<RequestInfo *>(TSmalloc(sizeof(RequestInfo)));
+
+  TSMBuffer hdr_url_buf;
+  TSMLoc hdr_url_loc;
+
+  TSHttpTxnClientReqGet(txnp, &hdr_url_buf, &hdr_url_loc);
+
+  // this only seems to be correct/consistent if done in http read request header state
+  char *url               = TSHttpTxnEffectiveUrlStringGet(txnp, &req_info->effective_url_length);
+  req_info->effective_url = TSstrndup(url, req_info->effective_url_length);
+  TSfree(url);
+
+  // copy the headers
+  req_info->http_hdr_buf = TSMBufferCreate();
+  TSHttpHdrClone(req_info->http_hdr_buf, hdr_url_buf, hdr_url_loc, &(req_info->http_hdr_loc));
+  // release the client request
+  TSHandleMLocRelease(hdr_url_buf, TS_NULL_MLOC, hdr_url_loc);
+
+  // It turns out that the client_addr field isn't used if the request
+  // is internal.  Cannot fetch a real client address in that case anyway
+  if (!TSHttpTxnIsInternal(txnp)) {
+    // copy client addr
+    req_info->client_addr = reinterpret_cast<struct sockaddr *>(TSmalloc(sizeof(struct sockaddr)));
+    memmove(req_info->client_addr, TSHttpTxnClientAddrGet(txnp), sizeof(struct sockaddr));
+  } else {
+    req_info->client_addr = nullptr;
+  }
+
+  // create the lookup key fron the effective url
+  MurmurHash3_x86_32(req_info->effective_url, req_info->effective_url_length, c_hashSeed, &(req_info->key_hash));
+
+  TSDebug(PLUGIN_TAG, "[%s] {%u} url=[%s]", __FUNCTION__, req_info->key_hash, req_info->effective_url);
+
+  return req_info;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+free_request_info(RequestInfo *req_info)
+{
+  TSfree(req_info->effective_url);
+  TSHandleMLocRelease(req_info->http_hdr_buf, TS_NULL_MLOC, req_info->http_hdr_loc);
+  TSMBufferDestroy(req_info->http_hdr_buf);
+  TSfree(req_info->client_addr);
+  TSfree(req_info);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static StateInfo *
+create_state_info(TSHttpTxn txnp, TSCont contp)
+{
+  StateInfo *state = new StateInfo(txnp, contp);
+  state->req_info  = create_request_info(txnp);
+  return state;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+free_state_info(StateInfo *state)
+{
+  // clean up states copy of url
+  if (state->pristine_url) {
+    TSfree(state->pristine_url);
+  }
+  state->pristine_url = nullptr;
+
+  // bunch of buffers state has created
+  if (state->req_io_buf_reader) {
+    TSIOBufferReaderFree(state->req_io_buf_reader);
+  }
+  state->req_io_buf_reader = nullptr;
+  if (state->req_io_buf) {
+    TSIOBufferDestroy(state->req_io_buf);
+  }
+  state->req_io_buf = nullptr;
+  if (state->resp_io_buf_reader) {
+    TSIOBufferReaderFree(state->resp_io_buf_reader);
+  }
+  state->resp_io_buf_reader = nullptr;
+  if (state->resp_io_buf) {
+    TSIOBufferDestroy(state->resp_io_buf);
+  }
+  state->resp_io_buf = nullptr;
+
+  // dont think these need cleanup
+  // state->r_vio
+  // state->w_vio
+  // state->vconn
+
+  // clean up request and response data state created
+  if (state->req_info) {
+    free_request_info(state->req_info);
+  }
+  state->req_info = nullptr;
+  if (state->resp_info) {
+    free_response_info(state->resp_info);
+  }
+  state->resp_info = nullptr;
+
+  // this should be null but check and delete
+  if (state->sie_body) {
+    delete state->sie_body;
+  }
+  state->sie_body = nullptr;
+
+  // this is a temp pointer do not delete
+  // state->cur_save_body
+
+  TSfree(state);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+int64_t
+aync_memory_total_add(ConfigInfo *plugin_config, int64_t change)
+{
+  int64_t total;
+  TSMutexLock(plugin_config->body_data_mutex);
+  plugin_config->body_data_memory_usage += change;
+  total                                  = plugin_config->body_data_memory_usage;
+  TSMutexUnlock(plugin_config->body_data_mutex);
+  return total;
+}
+/*-----------------------------------------------------------------------------------------------*/
+inline int64_t
+aync_memory_total_get(ConfigInfo *plugin_config)
+{
+  return aync_memory_total_add(plugin_config, 0);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+BodyData *
+async_check_active(uint32_t key_hash, ConfigInfo *plugin_config)
+{
+  BodyData *pFound = nullptr;
+  TSMutexLock(plugin_config->body_data_mutex);
+  UintBodyMap::iterator pos = plugin_config->body_data->find(key_hash);
+  if (pos != plugin_config->body_data->end()) {
+    pFound = pos->second;
+    ;
+  }
+  TSMutexUnlock(plugin_config->body_data_mutex);
+
+  TSDebug(PLUGIN_TAG, "[%s] {%u} pFound=%p", __FUNCTION__, key_hash, pFound);
+  return pFound;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+async_check_and_add_active(uint32_t key_hash, ConfigInfo *plugin_config)
+{
+  bool isNew = false;
+  TSMutexLock(plugin_config->body_data_mutex);
+  UintBodyMap::iterator pos = plugin_config->body_data->find(key_hash);
+  if (pos == plugin_config->body_data->end()) {
+    BodyData *pNew        = new BodyData();
+    pNew->key_hash        = key_hash;
+    pNew->key_hash_active = true;
+    plugin_config->body_data->insert(make_pair(key_hash, pNew));
+    isNew = true;
+  }
+  int tempSize = plugin_config->body_data->size();
+  TSMutexUnlock(plugin_config->body_data_mutex);
+
+  TSDebug(PLUGIN_TAG, "[%s] {%u} isNew=%d size=%d", __FUNCTION__, key_hash, isNew, tempSize);
+  return isNew;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+add_header(TSMBuffer &reqp, TSMLoc &hdr_loc, string header, string value)
+{
+  bool bReturn = false;
+  if (value.size() <= 0) {
+    TSDebug(PLUGIN_TAG, "\tWould set header %s to an empty value, skipping", header.c_str());
+  } else {
+    TSMLoc new_field;
+
+    if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(reqp, hdr_loc, header.data(), header.size(), &new_field)) {
+      if (TS_SUCCESS == TSMimeHdrFieldValueStringInsert(reqp, hdr_loc, new_field, -1, value.data(), value.size())) {
+        if (TS_SUCCESS == TSMimeHdrFieldAppend(reqp, hdr_loc, new_field)) {
+          TSDebug(PLUGIN_TAG, "\tAdded header %s: %s", header.c_str(), value.c_str());
+          bReturn = true;
+        }
+      } else {
+        TSMimeHdrFieldDestroy(reqp, hdr_loc, new_field);
+      }
+      TSHandleMLocRelease(reqp, hdr_loc, new_field);
+    }
+  }
+  return bReturn;
+}
+/*-----------------------------------------------------------------------------------------------*/
+bool
+async_remove_active(uint32_t key_hash, ConfigInfo *plugin_config)
+{
+  bool wasActive = false;
+  TSMutexLock(plugin_config->body_data_mutex);
+  UintBodyMap::iterator pos = plugin_config->body_data->find(key_hash);
+  if (pos != plugin_config->body_data->end()) {
+    plugin_config->body_data_memory_usage -= (pos->second)->getSize();
+    delete pos->second;
+    plugin_config->body_data->erase(pos);
+    wasActive = true;
+  }
+  int tempSize = plugin_config->body_data->size();
+  TSMutexUnlock(plugin_config->body_data_mutex);
+
+  TSDebug(PLUGIN_TAG, "[%s] {%u} wasActive=%d size=%d", __FUNCTION__, key_hash, wasActive, tempSize);
+  return wasActive;
+}
+/*-----------------------------------------------------------------------------------------------*/
+bool
+async_intercept_active(uint32_t key_hash, ConfigInfo *plugin_config)
+{
+  bool interceptActive = false;
+  TSMutexLock(plugin_config->body_data_mutex);
+  UintBodyMap::iterator pos = plugin_config->body_data->find(key_hash);
+  if (pos != plugin_config->body_data->end()) {
+    interceptActive = pos->second->intercept_active;
+  }
+  TSMutexUnlock(plugin_config->body_data_mutex);
+
+  TSDebug(PLUGIN_TAG, "[%s] {%u} interceptActive=%d", __FUNCTION__, key_hash, interceptActive);
+  return interceptActive;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+send_stale_response(StateInfo *state)
+{
+  // force to use age header
+  TSHttpTxnConfigIntSet(state->txnp, TS_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE, 1);
+  // add send response header hook for warning header
+  TSHttpTxnHookAdd(state->txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, state->transaction_contp);
+  // set cache as fresh
+  TSHttpTxnCacheLookupStatusSet(state->txnp, TS_CACHE_LOOKUP_HIT_FRESH);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static CachedHeaderInfo *
+get_cached_header_info(StateInfo *state)
+{
+  TSHttpTxn txnp = state->txnp;
+  CachedHeaderInfo *chi;
+  TSMBuffer cr_buf;
+  TSMLoc cr_hdr_loc, cr_date_loc, cr_cache_control_loc, cr_cache_control_dup_loc;
+  int cr_cache_control_count = 0;
+
+  chi = static_cast<CachedHeaderInfo *>(TSmalloc(sizeof(CachedHeaderInfo)));
+
+  chi->date    = 0;
+  chi->max_age = 0;
+
+  // -1 is used as a placeholder for teh following two meaning that their
+  // respective directives were not in the Cache-Control header.
+  chi->stale_while_revalidate = -1;
+  chi->stale_if_error         = -1;
+
+  if (TSHttpTxnCachedRespGet(txnp, &cr_buf, &cr_hdr_loc) == TS_SUCCESS) {
+    cr_date_loc = TSMimeHdrFieldFind(cr_buf, cr_hdr_loc, TS_MIME_FIELD_DATE, TS_MIME_LEN_DATE);
+    if (cr_date_loc != TS_NULL_MLOC) {
+      chi->date = TSMimeHdrFieldValueDateGet(cr_buf, cr_hdr_loc, cr_date_loc);
+      TSHandleMLocRelease(cr_buf, cr_hdr_loc, cr_date_loc);
+    }
+
+    cr_cache_control_loc = TSMimeHdrFieldFind(cr_buf, cr_hdr_loc, TS_MIME_FIELD_CACHE_CONTROL, TS_MIME_LEN_CACHE_CONTROL);
+
+    while (cr_cache_control_loc != TS_NULL_MLOC) {
+      cr_cache_control_count = TSMimeHdrFieldValuesCount(cr_buf, cr_hdr_loc, cr_cache_control_loc);
+
+      DirectiveParser directives;
+      for (int i = 0; i < cr_cache_control_count; ++i) {
+        int val_len   = 0;
+        char const *v = TSMimeHdrFieldValueStringGet(cr_buf, cr_hdr_loc, cr_cache_control_loc, i, &val_len);
+        TSDebug(PLUGIN_TAG, "Processing directives: %.*s", val_len, v);
+        swoc::TextView cache_control_value{v, static_cast<size_t>(val_len)};
+        directives.merge(DirectiveParser{cache_control_value});
+      }
+      TSDebug(PLUGIN_TAG, "max-age: %ld, stale-while-revalidate: %ld, stale-if-error: %ld", directives.get_max_age(),
+              directives.get_stale_while_revalidate(), directives.get_stale_if_error());
+      if (directives.get_max_age() >= 0) {
+        chi->max_age = directives.get_max_age();
+      }
+      if (directives.get_stale_while_revalidate() >= 0) {
+        chi->stale_while_revalidate = directives.get_stale_while_revalidate();
+      }
+      if (directives.get_stale_if_error() >= 0) {
+        chi->stale_if_error = directives.get_stale_if_error();
+      }
+
+      cr_cache_control_dup_loc = TSMimeHdrFieldNextDup(cr_buf, cr_hdr_loc, cr_cache_control_loc);
+      TSHandleMLocRelease(cr_buf, cr_hdr_loc, cr_cache_control_loc);
+      cr_cache_control_loc = cr_cache_control_dup_loc;
+    }
+    TSHandleMLocRelease(cr_buf, TS_NULL_MLOC, cr_hdr_loc);
+  }
+
+  TSDebug(PLUGIN_TAG, "[%s] item_count=%d max_age=%ld swr=%ld sie=%ld", __FUNCTION__, cr_cache_control_count, chi->max_age,
+          chi->stale_while_revalidate, chi->stale_if_error);
+
+  // load the config mins/default
+  if ((chi->stale_if_error == -1) && state->plugin_config->stale_if_error_default) {
+    chi->stale_if_error = state->plugin_config->stale_if_error_default;
+  }
+
+  if (state->plugin_config->stale_if_error_override > chi->stale_if_error) {
+    chi->stale_if_error = state->plugin_config->stale_if_error_override;
+  }
+
+  if ((chi->stale_while_revalidate == -1) && state->plugin_config->stale_while_revalidate_default) {
+    chi->stale_while_revalidate = state->plugin_config->stale_while_revalidate_default;
+  }
+
+  if (state->plugin_config->stale_while_revalidate_override > chi->stale_while_revalidate) {
+    chi->stale_while_revalidate = state->plugin_config->stale_while_revalidate_override;
+  }
+
+  // The callers use the stale-while-revalidate and stale-if-error values for
+  // calulations and do not expect nor need -1 values for non-existent
+  // directives as we did above. Now that we've handled the user configured
+  // defaults, we can assume "not set" is a value of 0.
+  chi->stale_while_revalidate = std::max(chi->stale_while_revalidate, 0l);
+  chi->stale_if_error         = std::max(chi->stale_if_error, 0l);
+
+  TSDebug(PLUGIN_TAG, "[%s] after defaults item_count=%d max_age=%ld swr=%ld sie=%ld", __FUNCTION__, cr_cache_control_count,
+          chi->max_age, chi->stale_while_revalidate, chi->stale_if_error);
+
+  return chi;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+fetch_save_response(StateInfo *state, BodyData *pBody)
+{
+  TSIOBufferBlock block;
+  int64_t avail;
+  char const *start;
+  block = TSIOBufferReaderStart(state->resp_io_buf_reader);
+  while (block != nullptr) {
+    start = TSIOBufferBlockReadStart(block, state->resp_io_buf_reader, &avail);
+    if (avail > 0) {
+      pBody->addChunk(start, avail);
+      // increase body_data_memory_usage only if content stored in plugin_config->body_data
+      if (pBody->key_hash_active) {
+        aync_memory_total_add(state->plugin_config, avail);
+      }
+    }
+    block = TSIOBufferBlockNext(block);
+  }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+fetch_parse_response(StateInfo *state)
+{
+  TSIOBufferBlock block;
+  TSParseResult pr = TS_PARSE_CONT;
+  int64_t avail;
+  char const *start;
+
+  block = TSIOBufferReaderStart(state->resp_io_buf_reader);
+
+  while ((pr == TS_PARSE_CONT) && (block != nullptr)) {
+    start = TSIOBufferBlockReadStart(block, state->resp_io_buf_reader, &avail);
+    if (avail > 0) {
+      pr = TSHttpHdrParseResp(state->resp_info->parser, state->resp_info->http_hdr_buf, state->resp_info->http_hdr_loc, &start,
+                              (start + avail));
+    }
+    block = TSIOBufferBlockNext(block);
+  }
+
+  if (pr != TS_PARSE_CONT) {
+    state->resp_info->status = TSHttpHdrStatusGet(state->resp_info->http_hdr_buf, state->resp_info->http_hdr_loc);
+    state->resp_info->parsed = true;
+    TSDebug(PLUGIN_TAG, "[%s] {%u} HTTP Status: %d", __FUNCTION__, state->req_info->key_hash, state->resp_info->status);
+  }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+fetch_read_the_data(StateInfo *state)
+{
+  // always save data
+  if (state->cur_save_body) {
+    fetch_save_response(state, state->cur_save_body);
+  } else {
+    TSDebug(PLUGIN_TAG_BAD, "[%s] no BodyData", __FUNCTION__);
+  }
+  // get the resp code
+  if (!state->resp_info->parsed) {
+    fetch_parse_response(state);
+  }
+  // Consume data
+  int64_t avail = TSIOBufferReaderAvail(state->resp_io_buf_reader);
+  TSIOBufferReaderConsume(state->resp_io_buf_reader, avail);
+  TSVIONDoneSet(state->r_vio, TSVIONDoneGet(state->r_vio) + avail);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+fetch_finish(StateInfo *state)
+{
+  TSDebug(PLUGIN_TAG, "[%s] {%u} swr=%d sie=%d", __FUNCTION__, state->req_info->key_hash, state->swr_active, state->sie_active);
+  if (state->swr_active) {
+    TSDebug(PLUGIN_TAG, "[%s] {%u} SWR Unlock URL / Post request", __FUNCTION__, state->req_info->key_hash);
+    if (state->sie_active && valid_sie_status(state->resp_info->status)) {
+      TSDebug(PLUGIN_TAG, "[%s] {%u} SWR Bad Data skipping", __FUNCTION__, state->req_info->key_hash);
+      if (!async_remove_active(state->req_info->key_hash, state->plugin_config)) {
+        TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} didnt delete async active", __FUNCTION__, state->req_info->key_hash);
+      }
+    } else {
+      // this will place the new data in cache by server intercept
+      intercept_fetch_the_url(state);
+    }
+  } else // state->sie_active
+  {
+    TSDebug(PLUGIN_TAG, "[%s] {%u} SIE in sync path Reenable %d", __FUNCTION__, state->req_info->key_hash,
+            state->resp_info->status);
+    if (valid_sie_status(state->resp_info->status)) {
+      TSDebug(PLUGIN_TAG, "[%s] {%u} SIE sending stale data", __FUNCTION__, state->req_info->key_hash);
+      if (state->plugin_config->log_info.object &&
+          (state->plugin_config->log_info.all || state->plugin_config->log_info.stale_if_error)) {
+        CachedHeaderInfo *chi = get_cached_header_info(state);
+        TSTextLogObjectWrite(state->plugin_config->log_info.object, "stale-if-error: %ld - %ld < %ld + %ld %s", state->txn_start,
+                             chi->date, chi->max_age, chi->stale_if_error, state->req_info->effective_url);
+        TSfree(chi);
+      }
+      // send out the stale data
+      send_stale_response(state);
+    } else {
+      TSDebug(PLUGIN_TAG, "[%s] SIE {%u} sending new data", __FUNCTION__, state->req_info->key_hash);
+      // load the data as if we are OS by ServerIntercept
+      BodyData *pBody = state->sie_body;
+      state->sie_body = nullptr;
+      // ServerIntercept will delete the body and send the data to the client
+      // Add sie_server_intercept header.
+      TSMBuffer buf;
+      TSMLoc hdr_loc;
+      TSHttpTxnClientReqGet(state->txnp, &buf, &hdr_loc);
+      if (!add_header(buf, hdr_loc, SIE_SERVER_INTERCEPT_HEADER, HTTP_VALUE_SERVER_INTERCEPT)) {
+        TSError("stale_response [%s] error inserting header %s", __FUNCTION__, SIE_SERVER_INTERCEPT_HEADER);
+      }
+      TSHandleMLocRelease(buf, TS_NULL_MLOC, hdr_loc);
+      serverInterceptSetup(state->txnp, pBody, state->plugin_config);
+      // This was TSHttpTxnNewCacheLookupDo -- now we just use the copy we have
+    }
+    TSHttpTxnReenable(state->txnp, TS_EVENT_HTTP_CONTINUE);
+  }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static int
+fetch_consume(TSCont contp, TSEvent event, void *edata)
+{
+  StateInfo *state;
+  state = static_cast<StateInfo *>(TSContDataGet(contp));
+
+  switch (event) {
+  case TS_EVENT_VCONN_WRITE_READY:
+    // We shouldn't get here because we specify the exact size of the buffer.
+    TSDebug(PLUGIN_TAG, "[%s] {%u} Write Ready", __FUNCTION__, state->req_info->key_hash);
+    // fallthrough
+  case TS_EVENT_VCONN_WRITE_COMPLETE:
+    TSDebug(PLUGIN_TAG, "[%s] {%u} Write Complete", __FUNCTION__, state->req_info->key_hash);
+    break;
+
+  case TS_EVENT_VCONN_READ_READY:
+    // save the data and parse header if needed
+    fetch_read_the_data(state);
+    TSVIOReenable(state->r_vio);
+    break;
+
+  case TS_EVENT_VCONN_READ_COMPLETE:
+  case TS_EVENT_VCONN_EOS:
+  case TS_EVENT_VCONN_INACTIVITY_TIMEOUT:
+  case TS_EVENT_ERROR:
+    // Don't free the reference to the state object
+    // The txnp object may already be freed at this point
+    if (event == TS_EVENT_VCONN_INACTIVITY_TIMEOUT) {
+      TSDebug(PLUGIN_TAG, "[%s] {%u} Inactivity Timeout", __FUNCTION__, state->req_info->key_hash);
+      TSVConnAbort(state->vconn, TS_VC_CLOSE_ABORT);
+    } else {
+      if (event == TS_EVENT_VCONN_READ_COMPLETE) {
+        TSDebug(PLUGIN_TAG, "[%s] {%u} Vconn Read Complete", __FUNCTION__, state->req_info->key_hash);
+      } else if (event == TS_EVENT_VCONN_EOS) {
+        TSDebug(PLUGIN_TAG, "[%s] {%u} Vconn Eos", __FUNCTION__, state->req_info->key_hash);
+      } else if (event == TS_EVENT_ERROR) {
+        TSDebug(PLUGIN_TAG, "[%s] {%u} Error Event", __FUNCTION__, state->req_info->key_hash);
+      }
+      TSVConnClose(state->vconn);
+    }
+
+    // I dont think we need this here but it should not hurt
+    fetch_read_the_data(state);
+    // we are done
+    fetch_finish(state);
+    // free state
+    free_state_info(state);
+    TSContDestroy(contp);
+    break;
+
+  default:
+    TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} Unknown event %d.", __FUNCTION__, state->req_info->key_hash, event);
+    break;
+  }
+
+  return 0;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static int
+fetch_resource(TSCont contp, TSEvent, void *)
+{
+  StateInfo *state;
+  TSCont consume_contp;
+  state = static_cast<StateInfo *>(TSContDataGet(contp));
+
+  TSDebug(PLUGIN_TAG, "[%s] {%u} Start swr=%d sie=%d ", __FUNCTION__, state->req_info->key_hash, state->swr_active,
+          state->sie_active);
+  consume_contp = TSContCreate(fetch_consume, TSMutexCreate());
+  TSContDataSet(consume_contp, state);
+
+  // create the response info swr may use this
+  state->resp_info = create_response_info();
+  // force a connection close header here seems to be needed
+  fix_connection_close(state);
+  // create some buffers
+  state->req_io_buf         = TSIOBufferCreate();
+  state->req_io_buf_reader  = TSIOBufferReaderAlloc(state->req_io_buf);
+  state->resp_io_buf        = TSIOBufferCreate();
+  state->resp_io_buf_reader = TSIOBufferReaderAlloc(state->resp_io_buf);
+  // add in my trailing parameter -- stripped off post cache lookup
+  add_trailing_parameter(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc);
+  // copy all the headers into a buffer
+  TSHttpHdrPrint(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, state->req_io_buf);
+  TSIOBufferWrite(state->req_io_buf, "\r\n", 2);
+
+  // setup place to store body data
+  if (state->sie_body) {
+    state->cur_save_body = state->sie_body;
+  } else {
+    state->cur_save_body = async_check_active(state->req_info->key_hash, state->plugin_config);
+  }
+
+  // connect , setup read , write
+  assert(state->req_info->client_addr != nullptr);
+  state->vconn = TSHttpConnect(state->req_info->client_addr);
+  state->r_vio = TSVConnRead(state->vconn, consume_contp, state->resp_io_buf, INT64_MAX);
+  state->w_vio =
+    TSVConnWrite(state->vconn, consume_contp, state->req_io_buf_reader, TSIOBufferReaderAvail(state->req_io_buf_reader));
+
+  TSContDestroy(contp);
+
+  return 0;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+fetch_start(StateInfo *state, TSCont contp)
+{
+  TSCont fetch_contp = nullptr;
+  TSDebug(PLUGIN_TAG, "[%s] {%u} Start swr=%d sie=%d ", __FUNCTION__, state->req_info->key_hash, state->swr_active,
+          state->sie_active);
+
+  ConfigInfo *plugin_config = static_cast<ConfigInfo *>(TSContDataGet(contp));
+
+  if (state->swr_active) {
+    bool isNew = async_check_and_add_active(state->req_info->key_hash, state->plugin_config);
+    // If already doing async lookup lets just close shop and go home
+    if (!isNew && !plugin_config->force_parallel_async) {
+      TSDebug(PLUGIN_TAG, "[%s] {%u} async in progress skip", __FUNCTION__, state->req_info->key_hash);
+      TSStatIntIncrement(state->plugin_config->rfc_stat_swr_hit_skip, 1);
+      // free state
+      TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+      free_state_info(state);
+    } else {
+      // get the pristine url for the server intercept
+      get_pristine_url(state);
+      fetch_contp = TSContCreate(fetch_resource, TSMutexCreate());
+      TSContDataSet(fetch_contp, state);
+      TSContScheduleOnPool(fetch_contp, 0, TS_THREAD_POOL_NET);
+    }
+  } else // state->sie_active
+  {
+    state->sie_body = new BodyData();
+    fetch_contp     = TSContCreate(fetch_resource, TSMutexCreate());
+    TSContDataSet(fetch_contp, state);
+    TSContScheduleOnPool(fetch_contp, 0, TS_THREAD_POOL_NET);
+  }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static int
+transaction_handler(TSCont contp, TSEvent event, void *edata)
+{
+  TSHttpStatus http_status = TS_HTTP_STATUS_NONE;
+  int status               = 0;
+  StateInfo *state         = nullptr;
+  TSMBuffer buf;
+  TSMLoc loc;
+
+  TSHttpTxn const txnp      = static_cast<TSHttpTxn>(edata);
+  ConfigInfo *plugin_config = static_cast<ConfigInfo *>(TSContDataGet(contp));
+  switch (event) {
+  case TS_EVENT_HTTP_READ_REQUEST_HDR:
+    TSDebug(PLUGIN_TAG, "[%s] TS_EVENT_HTTP_READ_REQUEST_HDR", __FUNCTION__);
+    assert(false);
+    break;
+
+  case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE:
+    state = static_cast<StateInfo *>(TSUserArgGet(txnp, plugin_config->txn_slot));
+
+    // If the state has already gone, just move on
+    if (!state) {
+      TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+      break;
+    }
+
+    // get the cache status default to miss
+    if (TSHttpTxnCacheLookupStatusGet(txnp, &status) != TS_SUCCESS) {
+      status = TS_CACHE_LOOKUP_MISS;
+      TSDebug(PLUGIN_TAG_BAD, "[%s] TSHttpTxnCacheLookupStatusGet failed", __FUNCTION__);
+    }
+
+    if (TSHttpTxnIsInternal(txnp)) {
+      bool cache_fresh = (status == TS_CACHE_LOOKUP_HIT_FRESH);
+      TSDebug(PLUGIN_TAG, "[%s] {%u} CacheLookupComplete Internal fresh=%d", __FUNCTION__, state->req_info->key_hash, cache_fresh);
+
+      // We dont want our internal requests to ever hit cache
+      if (cache_fresh && state->intercept_request) {
+        TSDebug(PLUGIN_TAG, "[%s] {%u} Set Cache to miss", __FUNCTION__, state->req_info->key_hash);
+        if (TSHttpTxnCacheLookupStatusSet(txnp, TS_CACHE_LOOKUP_MISS) != TS_SUCCESS) {
+          TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} TSHttpTxnCacheLookupStatusSet failed", __FUNCTION__, state->req_info->key_hash);
+        }
+      } else if (cache_fresh) // I dont think this can happen
+      {
+        TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} cache fresh not in stripped or intercept", __FUNCTION__, state->req_info->key_hash);
+      }
+
+      TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+      free_state_info(state);
+      TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_REQUEST_HDR_HOOK, contp);
+      TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+    } else {
+      if (status == TS_CACHE_LOOKUP_HIT_STALE) {
+        // Get headers setup chi -- free chi when done
+        CachedHeaderInfo *chi = get_cached_header_info(state);
+        state->swr_active =
+          ((((state->txn_start - chi->date) + 1) < (chi->max_age + chi->stale_while_revalidate)) && chi->stale_while_revalidate);
+        state->sie_active = ((((state->txn_start - chi->date) + 1) < (chi->max_age + chi->stale_if_error)) && chi->stale_if_error);
+        state->over_max_memory = (aync_memory_total_get(plugin_config) > plugin_config->max_body_data_memory_usage);
+
+        TSDebug(PLUGIN_TAG, "[%s] {%u} CacheLookup Stale swr=%d sie=%d over=%d", __FUNCTION__, state->req_info->key_hash,
+                state->swr_active, state->sie_active, state->over_max_memory);
+        // see if we are using too much memory and if so do not swr/sie
+        if (state->over_max_memory) {
+          TSDebug(PLUGIN_TAG, "[%s] {%u} Over memory Usage %" PRId64, __FUNCTION__, state->req_info->key_hash,
+                  aync_memory_total_get(plugin_config));
+          TSStatIntIncrement(state->plugin_config->rfc_stat_memory_over, 1);
+        }
+
+        if (state->swr_active) {
+          TSDebug(PLUGIN_TAG, "[%s] {%u} swr return stale - async refresh", __FUNCTION__, state->req_info->key_hash);
+          TSStatIntIncrement(plugin_config->rfc_stat_swr_hit, 1);
+          if (plugin_config->log_info.object && (plugin_config->log_info.all || plugin_config->log_info.stale_while_revalidate)) {
+            TSTextLogObjectWrite(plugin_config->log_info.object, "stale-while-revalidate: %ld - %ld < %ld + %ld [%s]",
+                                 state->txn_start, chi->date, chi->max_age, chi->stale_while_revalidate,
+                                 state->req_info->effective_url);
+          }
+          // send the data to the client
+          send_stale_response(state);
+          // do async if we are not over max
+          if (!state->over_max_memory) {
+            fetch_start(state, contp);
+          } else {
+            // since no fetch clean up state
+            TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+            free_state_info(state);
+          }
+          // reenable here
+          TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+        } else if (state->sie_active) {
+          TSDebug(PLUGIN_TAG, "[%s] {%u} sie wait response - return stale if 50x", __FUNCTION__, state->req_info->key_hash);
+          TSStatIntIncrement(plugin_config->rfc_stat_sie_hit, 1);
+          // lookup sync
+          if (!state->over_max_memory) {
+            fetch_start(state, contp);
+          } else // over max just send stale data reenable
+          {
+            send_stale_response(state);
+            // since no fetch clean up state and reenable
+            TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+            free_state_info(state);
+            TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+          }
+          // dont reenable here we are doing a sync call
+        } else {
+          // free state - reenable - had check
+          TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+          free_state_info(state);
+          TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+        }
+        TSfree(chi);
+      } else if (status != TS_CACHE_LOOKUP_HIT_FRESH) {
+        TSDebug(PLUGIN_TAG, "[%s] {%u} CacheLookup Miss/Skipped", __FUNCTION__, state->req_info->key_hash);
+
+        // this is just for stats
+        if (async_check_active(state->req_info->key_hash, plugin_config) != nullptr) {
+          TSDebug(PLUGIN_TAG, "[%s] {%u} not_stale aync in progress", __FUNCTION__, state->req_info->key_hash);
+          TSStatIntIncrement(plugin_config->rfc_stat_swr_miss_locked, 1);
+        }
+
+        // strip the async if we missed the internal fake cache lookup -- ats just misses ?
+        if (plugin_config->intercept_reroute) {
+          TSMBuffer buf;
+          TSMLoc hdr_loc;
+          TSHttpTxnClientReqGet(txnp, &buf, &hdr_loc);
+          if (strip_trailing_parameter(buf, hdr_loc)) {
+            TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} missed fake internal cache lookup", __FUNCTION__, state->req_info->key_hash);
+          }
+          TSHandleMLocRelease(buf, TS_NULL_MLOC, hdr_loc);
+        }
+
+        // free state - reenable - had check
+        TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+        free_state_info(state);
+        TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+      } else // TS_CACHE_LOOKUP_HIT_FRESH
+      {
+        TSDebug(PLUGIN_TAG, "[%s] {%u} CacheLookup Fresh", __FUNCTION__, state->req_info->key_hash);
+
+        // free state - reenable - had check
+        TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+        free_state_info(state);
+        TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+      }
+    }
+    break;
+
+  case TS_EVENT_HTTP_SEND_REQUEST_HDR: {
+    TSDebug(PLUGIN_TAG, "[%s]: strip_trailing_parameter", __FUNCTION__);
+    TSHttpTxnServerReqGet(txnp, &buf, &loc);
+    strip_trailing_parameter(buf, loc);
+    TSHandleMLocRelease(buf, TS_NULL_MLOC, loc);
+
+    // reenable
+    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+  }
+
+  break;
+
+  case TS_EVENT_HTTP_READ_RESPONSE_HDR:
+    // this should be internal request dont cache if valid sie error code -- no state variable
+    TSHttpTxnServerRespGet(txnp, &buf, &loc);
+    http_status = TSHttpHdrStatusGet(buf, loc);
+    if (valid_sie_status(http_status)) {
+      TSDebug(PLUGIN_TAG, "[%s] Set non-cachable %d", __FUNCTION__, http_status);
+      TSHttpTxnServerRespNoStoreSet(txnp, 1);
+    }
+    TSHandleMLocRelease(buf, TS_NULL_MLOC, loc);
+    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+    break;
+
+  case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
+    // add in the stale warning header -- no state variable
+    TSDebug(PLUGIN_TAG, "[%s] set warning header", __FUNCTION__);
+    TSHttpTxnClientRespGet(txnp, &buf, &loc);
+    if (!add_header(buf, loc, TS_MIME_FIELD_WARNING, HTTP_VALUE_STALE_WARNING)) {
+      TSError("stale_response [%s] error inserting header %s", __FUNCTION__, TS_MIME_FIELD_WARNING);
+    }
+    TSHandleMLocRelease(buf, TS_NULL_MLOC, loc);
+    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+    break;
+
+  default:
+    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+    break;
+  }
+
+  return 0;
+}
+
+ConfigInfo *
+parse_args(int argc, char const *argv[])
+{
+  if (argc <= 1) {
+    return nullptr;
+  }
+  ConfigInfo *plugin_config = new ConfigInfo();
+  int c;
+  optind                                = 1;
+  static const struct option longopts[] = {
+    {"log-all",                        no_argument,       nullptr, 'a'},
+    {"log-stale-while-revalidate",     no_argument,       nullptr, 'b'},
+    {"log-stale-if-error",             no_argument,       nullptr, 'c'},
+    {"log-filename",                   required_argument, nullptr, 'd'},
+
+    {"force-stale-if-error",           required_argument, nullptr, 'e'},
+    {"force-stale-while-revalidate",   required_argument, nullptr, 'f'},
+    {"stale-if-error-default",         required_argument, nullptr, 'g'},
+    {"stale-while-revalidate-default", required_argument, nullptr, 'h'},
+
+    {"intercept-reroute",              no_argument,       nullptr, 'i'},
+    {"max-memory-usage",               required_argument, nullptr, 'j'},
+    {"force-parallel-async",           no_argument,       nullptr, 'k'},
+
+ //  released a version that used "_" by mistake
+    {"force_stale_if_error",           required_argument, nullptr, 'E'},
+    {"force_stale_while_revalidate",   required_argument, nullptr, 'F'},
+    {"stale_if_error_default",         required_argument, nullptr, 'G'},
+    {"stale_while_revalidate_default", required_argument, nullptr, 'H'},
+
+    {nullptr,                          0,                 nullptr, 0  }
+  };
+
+  TSDebug(PLUGIN_TAG, "[%s] [%s]", __FUNCTION__, argv[1]);
+  while ((c = getopt_long(argc, const_cast<char *const *>(argv), "akref:EFGH:", longopts, nullptr)) != -1) {
+    switch (c) {
+    case 'a':
+      plugin_config->log_info.all = true;
+      break;
+    case 'b':
+      plugin_config->log_info.stale_while_revalidate = true;
+      break;
+    case 'c':
+      plugin_config->log_info.stale_if_error = true;
+      break;
+    case 'd':
+      plugin_config->log_info.filename = strdup(optarg);
+      break;
+
+    case 'e':
+    case 'E':
+      plugin_config->stale_if_error_override = atoi(optarg);
+      break;
+    case 'f':
+    case 'F':
+      plugin_config->stale_while_revalidate_override = atoi(optarg);
+      break;
+    case 'g':
+    case 'G':
+      plugin_config->stale_if_error_default = atoi(optarg);
+      break;
+    case 'h':
+    case 'H':
+      plugin_config->stale_while_revalidate_default = atoi(optarg);
+      break;
+
+    case 'i':
+      plugin_config->intercept_reroute = true;
+      break;
+
+    case 'j':
+      plugin_config->max_body_data_memory_usage = atoi(optarg);
+      break;
+
+    case 'k':
+      plugin_config->force_parallel_async = true;
+      break;
+
+    default:
+      break;
+    }
+  }
+
+  if (plugin_config->log_info.all || plugin_config->log_info.stale_while_revalidate || plugin_config->log_info.stale_if_error) {
+    TSDebug(PLUGIN_TAG, "[%s] Logging to %s", __FUNCTION__, plugin_config->log_info.filename);
+    TSTextLogObjectCreate(plugin_config->log_info.filename, TS_LOG_MODE_ADD_TIMESTAMP, &(plugin_config->log_info.object));
+  }
+
+  TSDebug(PLUGIN_TAG, "[%s] global stale if error override = %d", __FUNCTION__, (int)plugin_config->stale_if_error_override);
+  TSDebug(PLUGIN_TAG, "[%s] global stale while revalidate override = %d", __FUNCTION__,
+          (int)plugin_config->stale_while_revalidate_override);
+  TSDebug(PLUGIN_TAG, "[%s] global stale if error default = %d", __FUNCTION__, (int)plugin_config->stale_if_error_default);
+  TSDebug(PLUGIN_TAG, "[%s] global stale while revalidate default = %d", __FUNCTION__,
+          (int)plugin_config->stale_while_revalidate_default);
+  TSDebug(PLUGIN_TAG, "[%s] global intercept reroute = %d", __FUNCTION__, (int)plugin_config->intercept_reroute);
+  TSDebug(PLUGIN_TAG, "[%s] global force parallel async = %d", __FUNCTION__, (int)plugin_config->force_parallel_async);
+  TSDebug(PLUGIN_TAG, "[%s] global max memory usage = %" PRId64, __FUNCTION__, plugin_config->max_body_data_memory_usage);
+
+  return plugin_config;
+}
+
+static void
+read_request_header_handler(TSHttpTxn const txnp, ConfigInfo *plugin_config)
+{
+  TSCont transaction_contp = TSContCreate(transaction_handler, nullptr);
+  TSContDataSet(transaction_contp, plugin_config);
+  // todo: move state create to not always happen -- issue: effective url string seems to change in dif states
+  StateInfo *state = create_state_info(txnp, transaction_contp);
+  TSUserArgSet(txnp, plugin_config->txn_slot, state);
+
+  if (TSHttpTxnIsInternal(txnp)) {
+    // This is insufficient if there are other plugins using TSHttpConnect
+    TSDebug(PLUGIN_TAG, "[%s] {%u} ReadRequestHdr Internal", __FUNCTION__, state->req_info->key_hash);
+    BodyData *pBody = intercept_check_request(state);
+    if (pBody) {
+      TSDebug(PLUGIN_TAG, "[%s] {%u} ReadRequestHdr Intercept", __FUNCTION__, state->req_info->key_hash);
+      // key hash will have whats in header here
+      serverInterceptSetup(txnp, pBody, plugin_config);
+      state->intercept_request = true;
+    } else {
+      // not sure this is needed since we wont serve intercept in this case
+      TSDebug(PLUGIN_TAG, "[%s] {%u} ReadRequestHdr add response hook", __FUNCTION__, state->req_info->key_hash);
+      // dont cache if valid sie status code to myself
+      TSHttpTxnHookAdd(txnp, TS_HTTP_READ_RESPONSE_HDR_HOOK, transaction_contp);
+    }
+  } else {
+    // should we use the data we just cached -- this doesnt seem to help
+    if (plugin_config->intercept_reroute) {
+      // see if we are in the middle of intercepting
+      if (async_intercept_active(state->req_info->key_hash, plugin_config)) {
+        // add the async to the end so we use the fake cached response
+        TSMBuffer buf;
+        TSMLoc hdr_loc;
+        TSHttpTxnClientReqGet(txnp, &buf, &hdr_loc);
+        add_trailing_parameter(buf, hdr_loc);
+        TSHandleMLocRelease(buf, TS_NULL_MLOC, hdr_loc);
+        TSDebug(PLUGIN_TAG, "[%s] {%u} add async parm to get fake cached item", __FUNCTION__, state->req_info->key_hash);
+      }
+    }
+  }
+
+  // always cache lookup
+  TSHttpTxnHookAdd(txnp, TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, transaction_contp);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static int
+global_request_header_hook(TSCont contp, TSEvent event, void *edata)
+{
+  ConfigInfo *plugin_config = static_cast<ConfigInfo *>(TSContDataGet(contp));
+  TSHttpTxn const txnp      = static_cast<TSHttpTxn>(edata);
+  read_request_header_handler(txnp, plugin_config);
+  TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+  return TS_SUCCESS;
+}
+
+static void
+create_plugin_stats(ConfigInfo *plugin_config)
+{
+  plugin_config->rfc_stat_swr_hit =
+    TSStatCreate("stale_response.swr.hit", TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+  plugin_config->rfc_stat_swr_hit_skip =
+    TSStatCreate("stale_response.swr.hit.skip", TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+  plugin_config->rfc_stat_swr_miss_locked =
+    TSStatCreate("stale_response.swr.miss.locked", TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+  plugin_config->rfc_stat_sie_hit =
+    TSStatCreate("stale_response.sie.hit", TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+  plugin_config->rfc_stat_memory_over =
+    TSStatCreate("stale_response.memory.over", TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+TSPluginInit(int argc, const char *argv[])
+{
+  TSPluginRegistrationInfo info;
+  info.plugin_name   = const_cast<char *>(PLUGIN_TAG);
+  info.vendor_name   = const_cast<char *>(VENDOR_NAME);
+  info.support_email = const_cast<char *>(SUPPORT_EMAIL);
+
+  if (TS_SUCCESS != TSPluginRegister(&info)) {
+    TSError("Plugin registration failed.");
+    return;
+  }
+  TSDebug(PLUGIN_TAG, "Plugin registration succeeded.");
+
+  TSMgmtString value = nullptr;
+  TSMgmtStringGet("proxy.config.http.server_session_sharing.pool", &value);
+  if (nullptr == value || 0 != strcasecmp(value, "global")) {
+    TSError("[stale-response] Server session pool must be set to 'global'");
+    assert(false);
+  }
+
+  // create the default ConfigInfo
+  ConfigInfo *plugin_config = parse_args(argc, argv);
+
+  // proxy.config.http.insert_age_in_response
+  if (TS_SUCCESS != TSUserArgIndexReserve(TS_USER_ARGS_TXN, PLUGIN_TAG, "reserve state info slot", &(plugin_config->txn_slot))) {
+    TSError("stale_response [%s] failed to user argument data. Plugin registration failed.", PLUGIN_TAG);
+    return;
+  }
+  TSCont main_contp = TSContCreate(global_request_header_hook, nullptr);
+  TSContDataSet(main_contp, plugin_config);
+  TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, main_contp);
+
+  create_plugin_stats(plugin_config);
+
+  TSDebug(PLUGIN_TAG, "[%s] Plugin Init Complete", __FUNCTION__);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+// Remap support.
+
+/**
+ * Remap initialization.
+ */
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+  CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size);
+  TSDebug(PLUGIN_TAG, "[%s] Plugin Remap Init Complete", __FUNCTION__);
+
+  return TS_SUCCESS;
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char * /*errbuf */, int /* errbuf_size */)
+{
+  // second arg poses as the program name
+  --argc;
+  ++argv;
+  ConfigInfo *const plugin_config = parse_args(argc, const_cast<char const **>(argv));
+  *ih                             = static_cast<void *>(plugin_config);
+  if (TS_SUCCESS != TSUserArgIndexReserve(TS_USER_ARGS_TXN, PLUGIN_TAG, "reserve state info slot", &(plugin_config->txn_slot))) {
+    TSError("stale_response [%s] failed to user argument data. Plugin registration failed.", PLUGIN_TAG);
+    return TS_ERROR;
+  }
+  create_plugin_stats(plugin_config);
+  TSDebug(PLUGIN_TAG, "[%s] Plugin Remap New Instance Complete", __FUNCTION__);
+  return TS_SUCCESS;
+}
+
+void
+TSRemapDeleteInstance(void *ih)
+{
+  ConfigInfo *const plugin_config = static_cast<ConfigInfo *>(ih);
+  delete plugin_config;
+  TSDebug(PLUGIN_TAG, "[%s] Plugin Remap Delete Instance Complete", __FUNCTION__);
+}
+
+/**
+ * Remap entry point.
+ */
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo * /* rri */)
+{
+  ConfigInfo *plugin_config = static_cast<ConfigInfo *>(ih);
+  read_request_header_handler(txnp, plugin_config);
+  return TSREMAP_NO_REMAP;
+}
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/stale_response.h b/plugins/experimental/stale_response/stale_response.h
new file mode 100644
index 0000000000..9b642ba830
--- /dev/null
+++ b/plugins/experimental/stale_response/stale_response.h
@@ -0,0 +1,143 @@
+/** @file
+
+  Implements RFC 5861 (HTTP Cache-Control Extensions for Stale Content)
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include "ts/apidefs.h"
+#include "ts_wrap.h"
+#include "ts/ts.h"
+
+#include <cstdint>
+#include <map>
+
+struct BodyData;
+
+using UintBodyMap = std::map<uint32_t, BodyData *>;
+
+const unsigned int c_hashSeed = 99991;
+extern const char PLUGIN_TAG[];
+extern const char PLUGIN_TAG_BAD[];
+
+EXT_DBG_CTL(PLUGIN_TAG)
+EXT_DBG_CTL(PLUGIN_TAG_BAD)
+
+struct LogInfo {
+  TSTextLogObject object      = nullptr;
+  bool all                    = false;
+  bool stale_if_error         = false;
+  bool stale_while_revalidate = false;
+  char const *filename        = PLUGIN_TAG;
+};
+
+struct ConfigInfo {
+  ConfigInfo() : body_data{new UintBodyMap()}, body_data_mutex(TSMutexCreate()) {}
+  UintBodyMap *body_data = nullptr;
+  TSMutex body_data_mutex;
+  int64_t body_data_memory_usage = 0;
+  int txn_slot                   = 0;
+
+  bool intercept_reroute             = false;
+  bool force_parallel_async          = false;
+  int64_t max_body_data_memory_usage = c_default_max_body_data_memory_usage;
+
+  time_t stale_if_error_override         = 0;
+  time_t stale_while_revalidate_override = 0;
+  time_t stale_if_error_default          = 0;
+  time_t stale_while_revalidate_default  = 0;
+
+  int rfc_stat_swr_hit         = 0;
+  int rfc_stat_swr_hit_skip    = 0;
+  int rfc_stat_swr_miss_locked = 0;
+  int rfc_stat_sie_hit         = 0;
+  int rfc_stat_memory_over     = 0;
+
+  LogInfo log_info;
+
+private:
+  static constexpr int64_t c_default_max_body_data_memory_usage = 1024 * 1024 * 1024; // default to 1 GB
+};
+
+struct CachedHeaderInfo {
+  time_t date;
+  time_t stale_while_revalidate;
+  time_t stale_if_error;
+  time_t max_age;
+};
+
+struct RequestInfo {
+  char *effective_url;
+  int effective_url_length;
+  TSMBuffer http_hdr_buf;
+  TSMLoc http_hdr_loc;
+  struct sockaddr *client_addr;
+  uint32_t key_hash;
+};
+
+struct ResponseInfo {
+  TSMBuffer http_hdr_buf;
+  TSMLoc http_hdr_loc;
+  TSHttpParser parser;
+  bool parsed;
+  TSHttpStatus status;
+};
+
+struct StateInfo {
+  StateInfo(TSHttpTxn txnp, TSCont contp)
+    : txnp{txnp}, transaction_contp{contp}, plugin_config{static_cast<ConfigInfo *>(TSContDataGet(contp))}
+  {
+    time(&this->txn_start);
+  }
+  TSHttpTxn txnp                      = nullptr;
+  TSCont transaction_contp            = nullptr;
+  bool swr_active                     = false;
+  bool sie_active                     = false;
+  bool over_max_memory                = false;
+  TSIOBuffer req_io_buf               = nullptr;
+  TSIOBuffer resp_io_buf              = nullptr;
+  TSIOBufferReader req_io_buf_reader  = nullptr;
+  TSIOBufferReader resp_io_buf_reader = nullptr;
+  TSVIO r_vio                         = nullptr;
+  TSVIO w_vio                         = nullptr;
+  TSVConn vconn                       = nullptr;
+  RequestInfo *req_info               = nullptr;
+  ResponseInfo *resp_info             = nullptr;
+  time_t txn_start                    = 0;
+  ConfigInfo *plugin_config           = nullptr;
+  char *pristine_url                  = nullptr;
+  BodyData *sie_body                  = nullptr;
+  BodyData *cur_save_body             = nullptr;
+  bool intercept_request              = false;
+};
+
+BodyData *async_check_active(uint32_t key_hash, ConfigInfo *plugin_config);
+bool async_check_and_add_active(uint32_t key_hash, ConfigInfo *plugin_config);
+bool async_remove_active(uint32_t key_hash, ConfigInfo *plugin_config);
+
+// 500, 502, 503, 504
+inline bool
+valid_sie_status(TSHttpStatus status)
+{
+  return ((status == 500) || ((status >= 502) && (status <= 504)));
+}
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/ts_wrap.h b/plugins/experimental/stale_response/ts_wrap.h
new file mode 100644
index 0000000000..ba7d4abddf
--- /dev/null
+++ b/plugins/experimental/stale_response/ts_wrap.h
@@ -0,0 +1,42 @@
+/** @file
+
+  Wrapper for ts.h, to allow for compatibility with multiple major versions.
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include "ts/ts.h"
+
+#if TS_VERSION_MAJOR >= 10
+
+#define TSDebug(TAG_ID, ...) Dbg(stale_response_dbg_ctl_##TAG_ID, __VA_ARGS__)
+
+#define EXT_DBG_CTL(TAG_ID) extern DbgCtl stale_response_dbg_ctl_##TAG_ID;
+
+#define DEF_DBG_CTL(TAG_ID) DbgCtl stale_response_dbg_ctl_##TAG_ID{TAG_ID};
+
+#else
+
+#define EXT_DBG_CTL(TAG_ID)
+
+#define DEF_DBG_CTL(TAG_ID)
+
+#endif
diff --git a/plugins/experimental/CMakeLists.txt b/plugins/experimental/stale_response/unit_tests/CMakeLists.txt
similarity index 54%
copy from plugins/experimental/CMakeLists.txt
copy to plugins/experimental/stale_response/unit_tests/CMakeLists.txt
index a01426dc15..f1d73dae82 100644
--- a/plugins/experimental/CMakeLists.txt
+++ b/plugins/experimental/stale_response/unit_tests/CMakeLists.txt
@@ -15,27 +15,12 @@
 #
 #######################
 
-add_subdirectory(access_control)
-add_subdirectory(block_errors)
-add_subdirectory(cache_fill)
-add_subdirectory(cert_reporting_tool)
-add_subdirectory(cookie_remap)
-add_subdirectory(custom_redirect)
-add_subdirectory(fq_pacing)
-add_subdirectory(geoip_acl)
-add_subdirectory(header_freq)
-add_subdirectory(hook-trace)
-add_subdirectory(http_stats)
-add_subdirectory(icap)
-add_subdirectory(inliner)
-add_subdirectory(memcache)
-add_subdirectory(memory_profile)
-add_subdirectory(money_trace)
-add_subdirectory(mp4)
-add_subdirectory(rate_limit)
-add_subdirectory(redo_cache_lookup)
-add_subdirectory(sslheaders)
-add_subdirectory(stream_editor)
-add_subdirectory(system_stats)
-add_subdirectory(tls_bridge)
-add_subdirectory(url_sig)
+add_executable(
+  test_stale_response unit_test_main.cc test_DirectiveParser.cc test_BodyData.cc
+                      ${PROJECT_SOURCE_DIR}/DirectiveParser.cc
+)
+
+target_include_directories(test_stale_response PRIVATE "${PROJECT_SOURCE_DIR}")
+target_link_libraries(test_stale_response PRIVATE ts::tsutil libswoc::libswoc catch2::catch2)
+
+add_test(NAME test_stale_response COMMAND test_stale_response)
diff --git a/plugins/experimental/stale_response/unit_tests/test_BodyData.cc b/plugins/experimental/stale_response/unit_tests/test_BodyData.cc
new file mode 100644
index 0000000000..0fb4d52a16
--- /dev/null
+++ b/plugins/experimental/stale_response/unit_tests/test_BodyData.cc
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+#include "BodyData.h"
+
+#include <catch.hpp>
+#include <string>
+#include <string_view>
+
+DEF_DBG_CTL(PLUGIN_TAG_BODY)
+
+std::string_view DATA_PACKET0{"The quick brown fox jumps over the lazy dog"};
+std::string_view DATA_PACKET1{"Now is the time for all good men to come to the aid of their country"};
+
+TEST_CASE("Body Data")
+{
+  BodyData pBody;
+  REQUIRE(pBody.getSize() == 0);
+  REQUIRE(pBody.getChunkCount() == 0);
+  char const *pData = 0;
+  int64_t dataLen   = 0;
+  REQUIRE(pBody.getChunk(0, &pData, &dataLen) == false);
+
+  // First chunk.
+  pBody.addChunk(DATA_PACKET0.data(), DATA_PACKET0.size());
+  REQUIRE(pBody.getSize() == static_cast<int64_t>(DATA_PACKET0.size()));
+  REQUIRE(pBody.getChunkCount() == 1);
+  pBody.getChunk(0, &pData, &dataLen);
+  std::string dataRead0(pData, dataLen);
+  std::string dataOrig0(DATA_PACKET0.data(), DATA_PACKET0.size());
+  REQUIRE(dataRead0 == dataOrig0);
+
+  // Second chunk.
+  pBody.addChunk(DATA_PACKET1.data(), DATA_PACKET1.size());
+  REQUIRE(pBody.getSize() == static_cast<int64_t>(DATA_PACKET0.size() + DATA_PACKET1.size()));
+  REQUIRE(pBody.getChunkCount() == 2);
+  pBody.getChunk(1, &pData, &dataLen);
+  std::string dataRead1(pData, dataLen);
+  std::string dataOrig1(DATA_PACKET1.data(), DATA_PACKET1.size());
+  REQUIRE(dataRead1 == dataOrig1);
+
+  REQUIRE(pBody.removeChunk(0) == true);
+  REQUIRE(pBody.removeChunk(0) == false);
+  REQUIRE(pBody.removeChunk(1) == true);
+  REQUIRE(pBody.removeChunk(1) == false);
+  REQUIRE(pBody.removeChunk(97) == false);
+}
diff --git a/plugins/experimental/stale_response/unit_tests/test_DirectiveParser.cc b/plugins/experimental/stale_response/unit_tests/test_DirectiveParser.cc
new file mode 100644
index 0000000000..6556db5692
--- /dev/null
+++ b/plugins/experimental/stale_response/unit_tests/test_DirectiveParser.cc
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+
+#include "DirectiveParser.h"
+
+#include <catch.hpp>
+
+TEST_CASE("DirectiveParser Constructor")
+{
+  SECTION("Default constructor")
+  {
+    DirectiveParser parser;
+    REQUIRE(parser.get_max_age() == -1);
+    REQUIRE(parser.get_stale_while_revalidate() == -1);
+    REQUIRE(parser.get_stale_if_error() == -1);
+  }
+  SECTION("max-age")
+  {
+    DirectiveParser parser{"max-age=123"};
+    REQUIRE(parser.get_max_age() == 123);
+    REQUIRE(parser.get_stale_while_revalidate() == -1);
+    REQUIRE(parser.get_stale_if_error() == -1);
+  }
+  SECTION("stale-while-revalidate")
+  {
+    DirectiveParser parser{"stale-while-revalidate=123"};
+    REQUIRE(parser.get_max_age() == -1);
+    REQUIRE(parser.get_stale_while_revalidate() == 123);
+    REQUIRE(parser.get_stale_if_error() == -1);
+  }
+  SECTION("stale-if-error")
+  {
+    DirectiveParser parser{"stale-if-error=123"};
+    REQUIRE(parser.get_max_age() == -1);
+    REQUIRE(parser.get_stale_while_revalidate() == -1);
+    REQUIRE(parser.get_stale_if_error() == 123);
+  }
+  SECTION("other")
+  {
+    DirectiveParser parser{"s-maxage=123"};
+    REQUIRE(parser.get_max_age() == -1);
+    REQUIRE(parser.get_stale_while_revalidate() == -1);
+    REQUIRE(parser.get_stale_if_error() == -1);
+  }
+  SECTION("multiple")
+  {
+    DirectiveParser parser{"max-age=123, stale-while-revalidate=456, stale-if-error=789"};
+    REQUIRE(parser.get_max_age() == 123);
+    REQUIRE(parser.get_stale_while_revalidate() == 456);
+    REQUIRE(parser.get_stale_if_error() == 789);
+  }
+  SECTION("multiple with noise")
+  {
+    DirectiveParser parser{"max-age=123, s-maxage=456, stale-while-revalidate=789, must-understand, stale-if-error=012, public"};
+    REQUIRE(parser.get_max_age() == 123);
+    REQUIRE(parser.get_stale_while_revalidate() == 789);
+    REQUIRE(parser.get_stale_if_error() == 012);
+  }
+  SECTION("without commas")
+  {
+    DirectiveParser parser{"max-age=123 s-maxage=456 stale-while-revalidate=789 must-understand stale-if-error=012 public"};
+    REQUIRE(parser.get_max_age() == 123);
+    REQUIRE(parser.get_stale_while_revalidate() == 789);
+    REQUIRE(parser.get_stale_if_error() == 012);
+  }
+}
+
+TEST_CASE("DirectiveParser::merge")
+{
+  SECTION("Other replaces this")
+  {
+    DirectiveParser self{"max-age=123, stale-while-revalidate=456, stale-if-error=789"};
+    DirectiveParser other{"max-age=321, stale-while-revalidate=654, stale-if-error=987"};
+    self.merge(other);
+    REQUIRE(self.get_max_age() == 321);
+    REQUIRE(self.get_stale_while_revalidate() == 654);
+    REQUIRE(self.get_stale_if_error() == 987);
+  }
+  SECTION("Other unset does not replace this")
+  {
+    DirectiveParser self{"max-age=123, stale-while-revalidate=456, stale-if-error=789"};
+    DirectiveParser other{"max-age=321"};
+    self.merge(other);
+    REQUIRE(self.get_max_age() == 321);
+    REQUIRE(self.get_stale_while_revalidate() == 456);
+    REQUIRE(self.get_stale_if_error() == 789);
+  }
+}
diff --git a/plugins/experimental/stale_response/unit_tests/unit_test_main.cc b/plugins/experimental/stale_response/unit_tests/unit_test_main.cc
new file mode 100644
index 0000000000..6aed3a6309
--- /dev/null
+++ b/plugins/experimental/stale_response/unit_tests/unit_test_main.cc
@@ -0,0 +1,25 @@
+/** @file
+
+  This file used for catch based tests. It is the main() stub.
+
+  @section license License
+
+  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.
+ */
+
+#define CATCH_CONFIG_MAIN
+#include "catch.hpp"
diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response.test.py b/tests/gold_tests/pluginTest/stale_response/stale_response.test.py
new file mode 100644
index 0000000000..3d458b2f95
--- /dev/null
+++ b/tests/gold_tests/pluginTest/stale_response/stale_response.test.py
@@ -0,0 +1,173 @@
+'''
+Verify correct stale_response plugin behavior
+'''
+#  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.
+
+from enum import Enum
+import os
+
+Test.Summary = '''
+Verify correct stale_response plugin behavior
+'''
+
+Test.SkipUnless(Condition.PluginExists('stale_response.so'),)
+
+
+class OptionType(Enum):
+    """Describe what options to pass to the plugin."""
+
+    NONE = 0
+    DEFAULT_DIRECTIVES = 1
+    FORCE_SWR = 2
+    FORCE_SIE = 2
+
+
+class TestStaleResponse:
+    """Verify correct stale_response.so plugin behavior."""
+
+    _replay_file: str
+    _ts_counter: int = 0
+    _server_counter: int = 0
+    _client_counter: int = 0
+
+    def __init__(self, option_type: OptionType, is_global: bool) -> None:
+        """Initialize the test.
+
+        :param option_type: The type of options to pass to the stale_response plugin.
+        """
+        self._option_type = option_type
+        self._is_global = is_global
+
+        plugin_type_description = "global" if is_global else "per-remap"
+        if option_type == OptionType.NONE:
+            self._replay_file = "stale_response_no_default.replay.yaml"
+            option_description = f"no stale_response plugin options: {plugin_type_description}"
+        elif option_type == OptionType.DEFAULT_DIRECTIVES:
+            self._replay_file = "stale_response_with_defaults.replay.yaml"
+            option_description = f"--stale-while-revalidate-default 30 --stale-if-error-default 30: {plugin_type_description}"
+        elif option_type == OptionType.FORCE_SWR:
+            self._replay_file = "stale_response_with_force_swr.replay.yaml"
+            option_description = f"--force-stale-while-revalidate 30: {plugin_type_description}"
+        elif option_type == OptionType.FORCE_SIE:
+            self._replay_file = "stale_response_with_force_sie.replay.yaml"
+            option_description = f"--force-stale-if-error 30: {plugin_type_description}"
+
+        tr = Test.AddTestRun(f"stale_response.so Options: {option_description}")
+
+        self.setupOriginServer(tr)
+        self.setupTS()
+        self.setupClient(tr)
+        self.verify_plugin_log()
+
+    def setupOriginServer(self, tr: 'TestRun') -> None:
+        """ Configure the server.
+
+        :param tr: The test run to add the server to.
+        """
+        name = f'server_{TestStaleResponse._server_counter}'
+        TestStaleResponse._server_counter += 1
+        self._server = tr.AddVerifierServerProcess(name, self._replay_file)
+
+    def setupTS(self) -> None:
+        """Configure the traffic server.
+
+        ATS is not configured for a TestRun because we need it to last longer to
+        ensure that the plugin's log is created.
+        """
+        name = f'ts_{TestStaleResponse._ts_counter}'
+        TestStaleResponse._ts_counter += 1
+        ts = Test.MakeATSProcess(name)
+        self._ts = ts
+
+        log_path = os.path.join(ts.Variables.LOGDIR, 'stale_responses.log')
+        ts.Disk.File(log_path, id='stale_responses_log')
+
+        remap_plugin_config = ""
+        if self._is_global:
+            plugin_command = 'stale_response.so --log-all --log-filename stale_responses'
+            if self._option_type == OptionType.DEFAULT_DIRECTIVES:
+                plugin_command += ' --stale-while-revalidate-default 30 --stale-if-error-default 30'
+            elif self._option_type == OptionType.FORCE_SWR:
+                plugin_command += ' --force-stale-while-revalidate 30'
+            elif self._option_type == OptionType.FORCE_SIE:
+                plugin_command += ' --force-stale-if-error 30'
+            ts.Disk.plugin_config.AddLine(plugin_command)
+        else:
+            # Configure the stale_response plugin for the remap rule.
+            remap_plugin_config = "@plugin=stale_response.so @pparam=--log-all @pparam=--log-filename @pparam=stale_responses"
+            if self._option_type == OptionType.DEFAULT_DIRECTIVES:
+                remap_plugin_config += ' @pparam=--stale-while-revalidate-default @pparam=30 @pparam=--stale-if-error-default @pparam=30'
+            elif self._option_type == OptionType.FORCE_SWR:
+                remap_plugin_config += ' @pparam=--force-stale-while-revalidate @pparam=30'
+            elif self._option_type == OptionType.FORCE_SIE:
+                remap_plugin_config += ' @pparam=--force-stale-if-error @pparam=30'
+
+        ts.Disk.records_config.update(
+            {
+                "proxy.config.diags.debug.enabled": 1,
+                "proxy.config.diags.debug.tags": "http|stale_response",
+                "proxy.config.http.server_session_sharing.pool": "global",
+                # Turn off negative revalidating so that we can test stale-if-error.
+                "proxy.config.http.negative_revalidating_enabled": 0,
+            })
+        ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}/ {remap_plugin_config}")
+
+    def setupClient(self, tr: 'TestRun') -> None:
+
+        name = f'client_{TestStaleResponse._client_counter}'
+        TestStaleResponse._client_counter += 1
+        p = tr.AddVerifierClientProcess(
+            name, self._replay_file, http_ports=[self._ts.Variables.port], other_args='--thread-limit 1')
+        p.StartBefore(self._server)
+        p.StartBefore(self._ts)
+        p.StillRunningAfter = self._ts
+
+    def verify_plugin_log(self) -> None:
+        """Verify the contents of the stale_response plugin log."""
+        tr = Test.AddTestRun("Verify stale_response plugin log")
+        name = f'log_waiter_{TestStaleResponse._ts_counter}'
+        log_waiter = tr.Processes.Process(name)
+        log_waiter.Command = 'sleep 30'
+        if self._option_type == OptionType.FORCE_SWR:
+            log_waiter.Ready = When.FileContains(self._ts.Disk.stale_responses_log.Name, "stale-while-revalidate:")
+            self._ts.Disk.stale_responses_log.Content += Testers.ContainsExpression(
+                "stale-while-revalidate:.*stale.jpeg", "Verify stale-while-revalidate directive is logged")
+        elif self._option_type == OptionType.FORCE_SIE:
+            log_waiter.Ready = When.FileContains(self._ts.Disk.stale_responses_log.Name, "stale-if-error:")
+            self._ts.Disk.stale_responses_log.Content += Testers.ContainsExpression(
+                "stale-if-error:.*error.jpeg", "Verify stale-if-error directive is logged")
+        else:
+            log_waiter.Ready = When.FileContains(self._ts.Disk.stale_responses_log.Name, "stale-if-error:")
+            self._ts.Disk.stale_responses_log.Content += Testers.ContainsExpression(
+                "stale-while-revalidate:.*stale.jpeg", "Verify stale-while-revalidate directive is logged")
+            self._ts.Disk.stale_responses_log.Content += Testers.ContainsExpression(
+                "stale-if-error:.*error.jpeg", "Verify stale-if-error directive is logged")
+        p = tr.Processes.Default
+        p.Command = 'echo "Waiting upon the stale response log."'
+        p.StartBefore(log_waiter)
+        p.StillRunningAfter = self._ts
+
+
+TestStaleResponse(OptionType.NONE, is_global=True)
+TestStaleResponse(OptionType.DEFAULT_DIRECTIVES, is_global=True)
+TestStaleResponse(OptionType.FORCE_SWR, is_global=True)
+TestStaleResponse(OptionType.FORCE_SIE, is_global=True)
+
+TestStaleResponse(OptionType.NONE, is_global=False)
+TestStaleResponse(OptionType.DEFAULT_DIRECTIVES, is_global=False)
+TestStaleResponse(OptionType.FORCE_SWR, is_global=False)
+TestStaleResponse(OptionType.FORCE_SIE, is_global=False)
diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_no_default.replay.yaml b/tests/gold_tests/pluginTest/stale_response/stale_response_no_default.replay.yaml
new file mode 100644
index 0000000000..35d72c5973
--- /dev/null
+++ b/tests/gold_tests/pluginTest/stale_response/stale_response_no_default.replay.yaml
@@ -0,0 +1,175 @@
+#  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 tests the stale response plugin when no
+# --stale-while-revalidate-default and no --stale-if-error-default are
+# specified.
+
+sessions:
+
+- transactions:
+
+  - client-request:
+      method: GET
+      url: /pictures/stale.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, first-request ]
+
+    # Note: only a 1 second max-age.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Type, image/jpeg ]
+        - [ Content-Length, 100 ]
+        - [ Connection, keep-alive ]
+        - [ Cache-Control, max-age=1 ]
+        - [ X-Response, first-response ]
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ Cache-Control, { value: max-age=1, as: equal } ]
+        - [ X-Response, { value: first-response, as: equal } ]
+
+  - client-request:
+
+      # Now age it out.
+      delay: 2s
+
+      method: GET
+      url: /pictures/stale.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, second-request ]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Type, image/jpeg ]
+        - [ Content-Length, 100 ]
+        - [ Connection, keep-alive ]
+        # Note the stale-while-revalidate for the third response (not this).
+        # Also, verify that we can use comma separated directives.
+        - [ Cache-Control, "max-age=1, stale-while-revalidate=30" ]
+        - [ X-Response, second-response ]
+
+    # We better have gone back to the origin and gotten second-response.
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: second-response, as: equal } ]
+
+  - client-request:
+
+      # Now age it out, but expect stale-while-revalidate to override.
+      delay: 2s
+
+      method: GET
+      url: /pictures/stale.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, third-request ]
+
+    # We don't expect the request to go to the origin.
+    server-response:
+      status: 404
+      reason: Not Found
+      headers:
+        fields:
+        - [ X-Response, third-response ]
+
+    # Expect the stale second response because of stale-while-revalidate.
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: second-response, as: equal } ]
+
+  # Test stale-if-error.
+  - client-request:
+
+      method: GET
+      url: /pictures/error.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, fourth-request ]
+
+    # Populate a cached response for error.jpeg with stale-if-error.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Type, image/jpeg ]
+        - [ Content-Length, 100 ]
+        - [ Connection, keep-alive ]
+        - [ Cache-Control, max-age=1 stale-if-error=30 ]
+        - [ X-Response, fourth-response ]
+
+    # We better have gone back to the origin and gotten second-response.
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: fourth-response, as: equal } ]
+
+  # Now, age out the error.jpeg but reply with a 500. Expect the stale cached response.
+  - client-request:
+
+      delay: 2s
+
+      method: GET
+      url: /pictures/error.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, fifth-request ]
+
+    # Populate a cached response for error.jpeg with stale-if-error.
+    server-response:
+      status: 500
+      reason: Internal Server Error
+      headers:
+        fields:
+        - [ X-Response, fifth-response ]
+
+    # Expect the stale response due to stale-if-error.
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: fourth-response, as: equal } ]
diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_with_defaults.replay.yaml b/tests/gold_tests/pluginTest/stale_response/stale_response_with_defaults.replay.yaml
new file mode 100644
index 0000000000..d3523292b8
--- /dev/null
+++ b/tests/gold_tests/pluginTest/stale_response/stale_response_with_defaults.replay.yaml
@@ -0,0 +1,144 @@
+#  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 tests the stale response plugin when there are
+# --stale-while-revalidate-default and --stale-if-error-default specified.
+
+sessions:
+
+- transactions:
+
+  - client-request:
+      method: GET
+      url: /pictures/stale.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, first-request ]
+
+    # Note: only a 1 second max-age.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Type, image/jpeg ]
+        - [ Content-Length, 100 ]
+        - [ Connection, keep-alive ]
+        - [ Cache-Control, "max-age=1" ]
+        - [ X-Response, first-response ]
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ Cache-Control, { value: max-age=1, as: equal } ]
+        - [ X-Response, { value: first-response, as: equal } ]
+
+  - client-request:
+
+      # Now age it out.
+      delay: 2s
+
+      method: GET
+      url: /pictures/stale.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, second-request ]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Type, image/jpeg ]
+        - [ Content-Length, 100 ]
+        - [ Connection, keep-alive ]
+        - [ Cache-Control, max-age=1 ]
+        - [ X-Response, second-response ]
+
+    # Expect the cached response because of the default stale-while-revalidate.
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: first-response, as: equal } ]
+
+
+  # Test stale-if-error.
+  - client-request:
+
+      method: GET
+      url: /pictures/error.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, third-request ]
+
+    # Populate a cached response for error.jpeg with no stale-if-error.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Type, image/jpeg ]
+        - [ Content-Length, 100 ]
+        - [ Connection, keep-alive ]
+        # Explicitly turn off stale-while-revalidate so we can test the default stale-if-error.
+        - [ Cache-Control, "max-age=1, stale-while-revalidate=0" ]
+        - [ X-Response, third-response ]
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: third-response, as: equal } ]
+
+  # Now, age out the error.jpeg but reply with a 500. Expect the stale cached response.
+  - client-request:
+
+      delay: 2s
+
+      method: GET
+      url: /pictures/error.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, fourth-request ]
+
+    # Populate a cached response for error.jpeg with stale-if-error.
+    server-response:
+      status: 500
+      reason: Internal Server Error
+      headers:
+        fields:
+        - [ X-Response, fourth-response ]
+
+    # Expect the stale response due to default stale-if-error.
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: third-response, as: equal } ]
diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_sie.replay.yaml b/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_sie.replay.yaml
new file mode 100644
index 0000000000..10a8cd68f2
--- /dev/null
+++ b/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_sie.replay.yaml
@@ -0,0 +1,82 @@
+#  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 tests the stale response plugin when
+# --stale-if-error-default is configured.
+
+sessions:
+
+- transactions:
+
+  # Test stale-if-error.
+  - client-request:
+
+      method: GET
+      url: /pictures/error.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, first-request ]
+
+    # Populate a cached response for error.jpeg with stale-if-error.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Type, image/jpeg ]
+        - [ Content-Length, 100 ]
+        - [ Connection, keep-alive ]
+        # Configure a small stale-if-error.
+        - [ Cache-Control, max-age=1 stale-if-error=1 ]
+        - [ X-Response, first-response ]
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: first-response, as: equal } ]
+
+  # Now, age out the error.jpeg but reply with a 500. Expect the stale cached response.
+  - client-request:
+
+      delay: 3s
+
+      method: GET
+      url: /pictures/error.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, second-request ]
+
+    # Populate a cached response for error.jpeg with stale-if-error.
+    server-response:
+      status: 500
+      reason: Internal Server Error
+      headers:
+        fields:
+        - [ X-Response, second-response ]
+
+    # Expect the stale response due to stale-if-error.
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: first-response, as: equal } ]
diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_swr.replay.yaml b/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_swr.replay.yaml
new file mode 100644
index 0000000000..a980bb5010
--- /dev/null
+++ b/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_swr.replay.yaml
@@ -0,0 +1,86 @@
+#  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 tests the stale response plugin when
+# --force-stale-while-revalidate is configured.
+
+sessions:
+
+- transactions:
+
+  - client-request:
+      method: GET
+      url: /pictures/stale.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, first-request ]
+
+    # Note: only a 1 second max-age.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Type, image/jpeg ]
+        - [ Content-Length, 100 ]
+        - [ Connection, keep-alive ]
+        # The low stale-while-revalidate should be overridden by
+        # --force-stale-while-revalidate.
+        - [ Cache-Control, max-age=1 stale-while-revalidate=1 ]
+        - [ X-Response, first-response ]
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ Cache-Control, { value: "max-age=1 stale-while-revalidate=1", as: equal } ]
+        - [ X-Response, { value: first-response, as: equal } ]
+
+  - client-request:
+
+      # Now age it out.
+      delay: 3s
+
+      method: GET
+      url: /pictures/stale.jpeg
+      version: '1.1'
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ Content-Type, image/jpeg ]
+        - [ uuid, second-request ]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Content-Type, image/jpeg ]
+        - [ Content-Length, 100 ]
+        - [ Connection, keep-alive ]
+        - [ Cache-Control, max-age=1 ]
+        - [ X-Response, second-response ]
+
+    # Ensure that the forced larger stale-while-revalidate is used and we
+    # therefore get the cached response.
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Response, { value: first-response, as: equal } ]