You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2020/04/06 21:06:44 UTC

[trafficserver] branch 8.1.x updated: backport of slice plugin to 8.1.x

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

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


The following commit(s) were added to refs/heads/8.1.x by this push:
     new e9c7096  backport of slice plugin to 8.1.x
e9c7096 is described below

commit e9c70969b8034c7cba3e660166342836ef7eec3a
Author: Brian Olsen <br...@comcast.com>
AuthorDate: Mon Mar 30 16:38:02 2020 +0000

    backport of slice plugin to 8.1.x
---
 .gitignore                                         |   4 +
 doc/admin-guide/plugins/index.en.rst               |   6 +
 doc/admin-guide/plugins/slice.en.rst               | 256 +++++++++++
 plugins/Makefile.am                                |   1 +
 plugins/experimental/slice/Config.cc               | 206 +++++++++
 plugins/experimental/slice/Config.h                |  48 +++
 plugins/experimental/slice/ContentRange.cc         |  55 +++
 plugins/experimental/slice/ContentRange.h          |  53 +++
 plugins/experimental/slice/Data.cc                 |  65 +++
 plugins/experimental/slice/Data.h                  | 124 ++++++
 plugins/experimental/slice/HttpHeader.cc           | 363 ++++++++++++++++
 plugins/experimental/slice/HttpHeader.h            | 226 ++++++++++
 plugins/experimental/slice/Makefile.inc            |  64 +++
 plugins/experimental/slice/Makefile.tsxs           |  65 +++
 plugins/experimental/slice/README.md               | 165 ++++++++
 plugins/experimental/slice/Range.cc                | 196 +++++++++
 plugins/experimental/slice/Range.h                 |  78 ++++
 plugins/experimental/slice/Stage.h                 | 190 +++++++++
 plugins/experimental/slice/client.cc               | 165 ++++++++
 plugins/experimental/slice/client.h                |  36 ++
 plugins/experimental/slice/intercept.cc            |  82 ++++
 plugins/experimental/slice/intercept.h             |  23 +
 plugins/experimental/slice/response.cc             | 107 +++++
 plugins/experimental/slice/response.h              |  28 ++
 plugins/experimental/slice/server.cc               | 467 +++++++++++++++++++++
 plugins/experimental/slice/server.h                |  37 ++
 plugins/experimental/slice/slice.cc                | 308 ++++++++++++++
 plugins/experimental/slice/slice.h                 |  65 +++
 plugins/experimental/slice/slice_test.cc           | 195 +++++++++
 plugins/experimental/slice/transfer.cc             | 151 +++++++
 plugins/experimental/slice/transfer.h              |  37 ++
 .../experimental/slice/unit-tests/slice_test.cc    | 181 ++++++++
 .../experimental/slice/unit-tests/test_config.cc   |  86 ++++
 .../slice/unit-tests/test_content_range.cc         |  80 ++++
 .../experimental/slice/unit-tests/test_range.cc    |  99 +++++
 plugins/experimental/slice/util.cc                 | 134 ++++++
 plugins/experimental/slice/util.h                  |  36 ++
 tests/gold_tests/pluginTest/slice/curlsort.sh      |  33 ++
 .../pluginTest/slice/gold/slice_200.stderr.gold    |   1 +
 .../pluginTest/slice/gold/slice_200.stdout.gold    |   8 +
 .../pluginTest/slice/gold/slice_206.stderr.gold    |   1 +
 .../pluginTest/slice/gold/slice_206.stdout.gold    |   9 +
 .../pluginTest/slice/gold/slice_first.stderr.gold  |   1 +
 .../pluginTest/slice/gold/slice_first.stdout.gold  |   9 +
 .../pluginTest/slice/gold/slice_last.stderr.gold   |   1 +
 .../pluginTest/slice/gold/slice_last.stdout.gold   |   9 +
 .../pluginTest/slice/gold/slice_mid.stderr.gold    |   1 +
 .../pluginTest/slice/gold/slice_mid.stdout.gold    |   9 +
 .../pluginTest/slice/gold_error/crr.stderr.gold    |   1 +
 .../pluginTest/slice/gold_error/crr.stdout.gold    |   9 +
 .../pluginTest/slice/gold_error/etag.stderr.gold   |   1 +
 .../pluginTest/slice/gold_error/etag.stdout.gold   |   9 +
 .../pluginTest/slice/gold_error/lm.stderr.gold     |   1 +
 .../pluginTest/slice/gold_error/lm.stdout.gold     |   9 +
 .../pluginTest/slice/gold_error/non206.stderr.gold |   1 +
 .../pluginTest/slice/gold_error/non206.stdout.gold |  10 +
 tests/gold_tests/pluginTest/slice/slice.test.py    | 183 ++++++++
 .../pluginTest/slice/slice_error.test.py           | 322 ++++++++++++++
 58 files changed, 5110 insertions(+)

diff --git a/.gitignore b/.gitignore
index 1e8da8b..c6ef414 100644
--- a/.gitignore
+++ b/.gitignore
@@ -119,6 +119,10 @@ plugins/esi/processor_test
 plugins/esi/utils_test
 plugins/esi/vars_test
 
+plugins/experimental/slice/test_config
+plugins/experimental/slice/test_content_range
+plugins/experimental/slice/test_range
+
 mgmt/api/traffic_api_cli_remote
 mgmt/tools/traffic_mcast_snoop
 mgmt/tools/traffic_net_config
diff --git a/doc/admin-guide/plugins/index.en.rst b/doc/admin-guide/plugins/index.en.rst
index c345f52..6880c6b 100644
--- a/doc/admin-guide/plugins/index.en.rst
+++ b/doc/admin-guide/plugins/index.en.rst
@@ -62,6 +62,7 @@ Plugins that are considered stable are installed by default in |TS| releases.
    Regex Remap <regex_remap.en>
    Regex Revalidate <regex_revalidate.en>
    Remap Purge <remap_purge.en>
+   Slice <slice.en>
    Stats over HTTP <stats_over_http.en>
    TCPInfo <tcpinfo.en>
    XDebug <xdebug.en>
@@ -117,6 +118,11 @@ Plugins that are considered stable are installed by default in |TS| releases.
 :doc:`Regex Revalidate <regex_revalidate.en>`
    Configurable rules for forcing cache object revalidations using regular expressions.
 
+:doc:`Slicer <slice.en>`
+   Slice full file or range based requests into deterministic chunks,
+   allowing large files to be spread across multiple cache stripes. Allows
+   range requests to be satisfied by stitching these chunks together.
+
 :doc:`Stats over HTTP <stats_over_http.en>`
     Provide an HTTP interface to all |TS| statistics.
 
diff --git a/doc/admin-guide/plugins/slice.en.rst b/doc/admin-guide/plugins/slice.en.rst
new file mode 100644
index 0000000..b131178
--- /dev/null
+++ b/doc/admin-guide/plugins/slice.en.rst
@@ -0,0 +1,256 @@
+.. 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.
+
+.. _admin-plugins-slice:
+
+Slice Plugin
+***************
+
+This plugin takes client requests and breaks them up into
+successive aligned block requests.  This supports both
+whole asset and single range requests.
+
+Purpose
+=======
+
+This slice plugin, along with the `cache_range_requests`
+plugin allows the following:
+
+-  Fulfill arbitrary range requests by fetching a minimum
+   number of cacheable aligned blocks to fulfill the request.
+-  Breaks up very large assets into much smaller cache
+   blocks that can be spread across multiple storage
+   devices and within cache groups.
+
+Configuration
+=============
+
+This plugin is intended for use as a remap plugin and is
+configured in :file:`remap.config`.
+
+Or preferably per remap rule in :file:`remap.config`::
+
+    map http://ats/ http://parent/ @plugin=slice.so \
+        @plugin=cache_range_requests.so
+
+In this case, the plugin will use the default behaviour:
+
+-  Fulfill whole file or range requests by requesting cacheable
+   block aligned ranges from the parent and assemble them
+   into client responses, either 200 or 206 depending on the
+   client request.
+-  Default block size is 1mb (1048576 bytes).
+-  This plugin depends on the cache_range_requests plugin
+   to perform actual parent fetching and block caching
+   and If-* conditional header evaluations.
+
+Plugin Options
+--------------
+
+The slice plugin supports the following options::
+
+    --blockbytes=<bytes> (optional)
+        Default is 1m or 1048576 bytes
+        -b <bytes> for short.
+        Suffix k,m,g supported
+        Limited to 32k and 32m inclusive.
+
+    --blockbytes-test=<bytes> (optional)
+        Suffix k,m,g supported
+        -t <bytes> for short.
+        Limited to any positive number.
+        Ignored if --blockbytes provided.
+
+    --remap-host=<loopback hostname> (optional)
+        Uses effective url with given hostname for remapping.
+        Requires setting up an intermediate loopback remap rule.
+        -r for short
+
+    --pace-errorlog=<seconds> (optional)
+        Limit stitching error logs to every 'n' second(s)
+        -p for short
+
+    --disable-errorlog (optional)
+        Disable writing block stitch errors to the error log.
+        -d for short
+
+
+Examples::
+
+    @plugin=slice.so @pparam=--blockbytes=1000000 @plugin=cache_range_requests.so
+
+Or alternatively::
+
+    @plugin=slice.so @pparam=-b @pparam=1000000 @plugin=cache_range_requests.so
+
+Byte suffix examples::
+
+    slice.so --blockbytes=5m
+    slice.so -b 512k
+    slice.so --blockbytes=32m
+
+For testing and extreme purposes the parameter ``blockbytes-test`` may
+be used instead which is unchecked::
+
+    slice.so --blockbytes-test=1G
+    slice.so -t 13
+
+Because the slice plugin is susceptible to errors during block stitching
+extra logs related to stitching are written to ``diags.log``.  Worst case
+an error log entry could be generated for every transaction.  The
+following options are provided to help with log overrun::
+
+    slice.so --pace-errorlog=5
+    slice.so -p 1
+    slice.so --disable-errorlog
+
+After modifying :file:`remap.config`, restart or reload traffic server
+(sudo traffic_ctl config reload) or (sudo traffic_ctl server restart)
+to activate the new configuration values.
+
+Debug Options
+-------------
+
+While the current slice plugin is able to detect block consistency
+errors during the block stitching process, it can only abort the
+client connection.  A CDN can only "fix" these by issuing an appropriate
+content revalidation.
+
+Under normal logging these slice block errors tend to show up as::
+
+    pscl value 0
+    crc value ERR_READ_ERROR
+
+By default more detailed stitching errors are written to ``diags.log``.
+Examples are as follows::
+
+ERROR: [slice.cc: 288] logSliceError(): 1555705573.639 reason="Non 206 internal block response" uri="http://ats_ep/someasset.mp4" uas="curl" req_range="bytes=1000000-" norm_range="bytes 1000000-52428799/52428800" etag_exp="%221603934496%22" lm_exp="Fri, 19 Apr 2019 18:53:20 GMT" blk_range="21000000-21999999" status_got="206" cr_got="" etag_got="%221603934496%22" lm_got="" cc="no-store" via=""
+
+ERROR: [server.cc: 288] logSliceError(): 1572370000.219 reason="Mismatch block Etag" uri="http://ats_ep/someasset.mp4" uas="curl" req_range="bytes=1092779033-1096299354" norm_range="bytes 1092779033-1096299354/2147483648" etag_exp="%223719843648%22" lm_exp="Tue, 29 Oct 2019 14:40:00 GMT" blk_range="1095000000-1095999999" status_got="206" cr_got="bytes 1095000000-1095999999/2147483648" etag_got="%223719853648%22" lm_got="Tue, 29 Oct 2019 17:26:40 GMT" cc="max-age=10000" via=""
+
+Whether or how often these detailed log entries are written are
+configurable plugin options.
+
+Implementation Notes
+====================
+
+This slice plugin is a stop gap plugin for handling special cases
+involving very large assets that may be range requested. Hopefully
+the slice plugin is deprecated in the future when partial object
+caching is finally implemented.
+
+Slice *ONLY* handles slicing up requests into blocks, it delegates
+actual caching and fetching to the cache_range_requests.so plugin.
+
+Plugin Function
+---------------
+
+Below is a quick functional outline of how a request is served
+by a remap rule containing the Slice plugin with cache_range_requests:
+
+For each client request that comes in all remap plugins are run up
+until the slice plugin is hit.  If the slice plugin *can* be run (ie:
+GET request) it will handle the request and STOP any further plugins
+from executing.
+
+At this point the request is sliced into 1 or more blocks by
+adding in range request headers ("Range: bytes=").  A special
+header X-Slicer-Info header is added and the pristine URL is
+restored.
+
+For each of these blocks separate sequential TSHttpConnect(s) are made
+back into the front end of ATS and all of the remap plugins are rerun.
+Slice skips the remap due to presence of the X-Slicer-Info header and
+allows cache_range_requests.so to serve the slice block back to Slice
+either via cache OR parent request.
+
+Slice assembles a header based on the first slice block response and
+sends it to the client.  If necessary it then skips over bytes in
+the first block and starts sending byte content, examining each
+block header and sends its bytes to the client until the client
+request is satisfied.
+
+Any extra bytes at the end of the last block are consumed by
+the the Slice plugin to allow cache_range_requests to finish
+the block fetch to ensure the block is cached.
+
+Important Notes
+===============
+
+This plugin assumes that the content requested is cacheable.
+
+Any first block server response that is not a 206 is passed directly
+down to the client.  If that response is a '200' only the first
+portion of the response is passed back and the transaction is closed.
+
+Only the first server response block is used to evaluate any "If-"
+conditional headers.  Subsequent server slice block requests
+remove these headers.
+
+The only 416 response that this plugin handles itself is if the
+requested range is inside the last slice block but past the end of
+the asset contents.  Other 416 responses are handled by the parent.
+
+If a client aborts mid transaction the current slice block continues to
+be read from the server until it is complete to ensure that the block
+is cached.
+
+Slice *always* makes ``blockbytes`` sized requests which are handled
+by cache_range_requests.  The parent will trim those requests to
+account for the asset Content-Length so only the appropriate number
+of bytes are actually transferred and cached.
+
+Effective URL remap
+===================
+
+By default the plugin restores the Pristine Url which reuses the same
+remap rule for each slice block.  This is wasteful in that it reruns
+the previous remap rules, and those remap rules must be smart enough to
+check for the existence of any headers they may have created the first
+time they have were visited.
+
+To get around this the '--remap-host=<host>' or '-r <host>' option may
+be used.  This requires an intermediate loopback remap to be defined which
+handles each slice block request.
+
+This works well with any remap rules that use the url_sig or uri_signing
+plugins.  As the client remap rule is not caching any plugins that
+manipulate the cache key would need to go into the loopback to parent
+remap rule.
+
+NOTE: Requests NOT handled by the slice plugin (ie: HEAD requests) are
+handled as with a typical remap rule.  GET requests intercepted by the
+slice plugin are virtually reissued into ATS and are proxied through
+another remap rule which must contain the ``cache_range_requests`` plugin
+
+Examples::
+
+    map http://ats/ http://parent/ @plugin=slice.so @pparam=--remap-host=loopback
+    map http://loopback/ http://parent/ @plugin=cache_range_requests.so
+
+Alternatively::
+
+    map http://ats/ http://parent/ @plugin=slice.so @pparam=-r @pparam=loopback
+    map http://loopback/ http://parent/ @plugin=cache_range_requests.so
+
+Current Limitations
+===================
+
+Since the Slice plugin is written as an intercept handler it loses the
+ability to use normal state machine hooks and transaction states. This
+functionality is handled by using the ``cache_range_requests`` plugin
+to interact with ATS.
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 802f296..6520149 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -74,6 +74,7 @@ include experimental/mp4/Makefile.inc
 include experimental/multiplexer/Makefile.inc
 include experimental/remap_purge/Makefile.inc
 include experimental/server_push_preload/Makefile.inc
+include experimental/slice/Makefile.inc
 include experimental/ssl_cert_loader/Makefile.inc
 include experimental/sslheaders/Makefile.inc
 include experimental/stale_while_revalidate/Makefile.inc
diff --git a/plugins/experimental/slice/Config.cc b/plugins/experimental/slice/Config.cc
new file mode 100644
index 0000000..fb04ebb
--- /dev/null
+++ b/plugins/experimental/slice/Config.cc
@@ -0,0 +1,206 @@
+/** @file
+  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 "Config.h"
+
+#include <cctype>
+#include <cinttypes>
+#include <cstdlib>
+#include <getopt.h>
+#include <string_view>
+
+#include "ts/experimental.h"
+
+int64_t
+Config::bytesFrom(char const *const valstr)
+{
+  char *endptr          = nullptr;
+  int64_t blockbytes    = strtoll(valstr, &endptr, 10);
+  constexpr int64_t kib = 1024;
+
+  if (nullptr != endptr && valstr < endptr) {
+    size_t const dist = endptr - valstr;
+    if (dist < strlen(valstr) && 0 <= blockbytes) {
+      switch (tolower(*endptr)) {
+      case 'g':
+        blockbytes *= (kib * kib * kib);
+        break;
+      case 'm':
+        blockbytes *= (kib * kib);
+        break;
+      case 'k':
+        blockbytes *= kib;
+        break;
+      default:
+        break;
+      }
+    }
+  }
+
+  if (blockbytes < 0) {
+    blockbytes = 0;
+  }
+
+  return blockbytes;
+}
+
+bool
+Config::fromArgs(int const argc, char const *const argv[])
+{
+  DEBUG_LOG("Number of arguments: %d", argc);
+  for (int index = 0; index < argc; ++index) {
+    DEBUG_LOG("args[%d] = %s", index, argv[index]);
+  }
+
+  // look for lowest priority deprecated blockbytes
+  int64_t blockbytes = 0;
+
+  // backwards compat: look for blockbytes
+  for (int index = 0; index < argc; ++index) {
+    std::string_view const argstr = argv[index];
+
+    std::size_t const spos = argstr.find_first_of(':');
+    if (spos != std::string_view::npos) {
+      std::string_view const key = argstr.substr(0, spos);
+      std::string_view const val = argstr.substr(spos + 1);
+
+      if (!key.empty() && !val.empty()) {
+        char const *const valstr = val.data(); // inherits argv's null
+        int64_t const bytesread  = bytesFrom(valstr);
+
+        if (blockbytesmin <= bytesread && bytesread <= blockbytesmax) {
+          DEBUG_LOG("Found deprecated blockbytes %" PRId64, bytesread);
+          blockbytes = bytesread;
+        }
+      }
+    }
+  }
+
+  // standard parsing
+  constexpr struct option longopts[] = {
+    {const_cast<char *>("blockbytes"), required_argument, nullptr, 'b'},
+    {const_cast<char *>("blockbytes-test"), required_argument, nullptr, 't'},
+    {const_cast<char *>("remap-host"), required_argument, nullptr, 'r'},
+    {const_cast<char *>("pace-errorlog"), required_argument, nullptr, 'p'},
+    {const_cast<char *>("disable-errorlog"), no_argument, nullptr, 'd'},
+    {const_cast<char *>("throttle"), no_argument, nullptr, 'o'},
+    {nullptr, 0, nullptr, 0},
+  };
+
+  // getopt assumes args start at '1' so this hack is needed
+  char *const *argvp = (const_cast<char *const *>(argv) - 1);
+  for (;;) {
+    int const opt = getopt_long(argc + 1, argvp, "b:t:r:p:do", longopts, nullptr);
+    if (-1 == opt) {
+      break;
+    }
+
+    DEBUG_LOG("processing '%c' %s", (char)opt, argvp[optind - 1]);
+
+    switch (opt) {
+    case 'b': {
+      int64_t const bytesread = bytesFrom(optarg);
+      if (blockbytesmin <= bytesread && bytesread <= blockbytesmax) {
+        DEBUG_LOG("Using blockbytes %" PRId64, bytesread);
+        blockbytes = bytesread;
+      } else {
+        ERROR_LOG("Invalid blockbytes: %s", optarg);
+      }
+    } break;
+    case 't': {
+      if (0 == blockbytes) {
+        int64_t const bytesread = bytesFrom(optarg);
+        if (0 < bytesread) {
+          DEBUG_LOG("Using blockbytes-test %" PRId64, bytesread);
+          blockbytes = bytesread;
+        } else {
+          ERROR_LOG("Invalid blockbytes-test: %s", optarg);
+        }
+      } else {
+        DEBUG_LOG("Skipping blockbytes-test in favor of blockbytes");
+      }
+    } break;
+    case 'r':
+      m_remaphost = optarg;
+      DEBUG_LOG("Using loopback remap host override: %s", m_remaphost.c_str());
+      break;
+    case 'p': {
+      int const secsread = atoi(optarg);
+      if (0 < secsread) {
+        m_paceerrsecs = std::min(secsread, 60);
+      } else {
+        ERROR_LOG("Ignoring pace-errlog argument");
+      }
+    } break;
+    case 'd':
+      m_paceerrsecs = -1;
+      break;
+    case 'o':
+      m_throttle = true;
+      DEBUG_LOG("Enabling internal block throttling");
+      break;
+    default:
+      break;
+    }
+  }
+
+  if (0 < blockbytes) {
+    DEBUG_LOG("Using configured blockbytes %" PRId64, blockbytes);
+    m_blockbytes = blockbytes;
+  } else {
+    DEBUG_LOG("Using default blockbytes %" PRId64, m_blockbytes);
+  }
+
+  if (m_paceerrsecs < 0) {
+    DEBUG_LOG("Block stitching error logs disabled");
+  } else if (0 == m_paceerrsecs) {
+    DEBUG_LOG("Block stitching error logs enabled");
+  } else {
+    DEBUG_LOG("Block stitching error logs at most every %d sec(s)", m_paceerrsecs);
+  }
+
+  return true;
+}
+
+bool
+Config::canLogError()
+{
+  if (m_paceerrsecs < 0) {
+    return false;
+  } else if (0 == m_paceerrsecs) {
+    return true;
+  }
+
+#if !defined(UNITTEST)
+  TSHRTime const timenow = TShrtime();
+#endif
+
+  std::lock_guard<std::mutex> const guard(m_mutex);
+
+#if !defined(UNITTEST)
+  if (timenow < m_nextlogtime) {
+    return false;
+  }
+
+  m_nextlogtime = timenow + TS_HRTIME_SECONDS(m_paceerrsecs);
+#else
+  m_nextlogtime = 0; // needed by clang
+#endif
+
+  return true;
+}
diff --git a/plugins/experimental/slice/Config.h b/plugins/experimental/slice/Config.h
new file mode 100644
index 0000000..42de33d
--- /dev/null
+++ b/plugins/experimental/slice/Config.h
@@ -0,0 +1,48 @@
+/** @file
+  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 "slice.h"
+
+#include <mutex>
+
+// Data Structures and Classes
+struct Config {
+  static constexpr int64_t const blockbytesmin     = 1024 * 256;       // 256KB
+  static constexpr int64_t const blockbytesmax     = 1024 * 1024 * 32; // 32MB
+  static constexpr int64_t const blockbytesdefault = 1024 * 1024;      // 1MB
+
+  int64_t m_blockbytes{blockbytesdefault};
+  std::string m_remaphost; // remap host to use for loopback slice GET
+  bool m_throttle{false};  // internal block throttling
+  int m_paceerrsecs{0};    // -1 disable logging, 0 no pacing, max 60s
+
+  // Convert optarg to bytes
+  static int64_t bytesFrom(char const *const valstr);
+
+  // Parse from args, ast one wins
+  bool fromArgs(int const argc, char const *const argv[]);
+
+  // Check if the error should can be logged, if sucessful may update m_nexttime
+  bool canLogError();
+
+private:
+  TSHRTime m_nextlogtime{0}; // next time to log in ns
+  std::mutex m_mutex;
+};
diff --git a/plugins/experimental/slice/ContentRange.cc b/plugins/experimental/slice/ContentRange.cc
new file mode 100644
index 0000000..c9d9edd
--- /dev/null
+++ b/plugins/experimental/slice/ContentRange.cc
@@ -0,0 +1,55 @@
+/** @file
+  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 "ContentRange.h"
+
+#include <cinttypes>
+#include <cstdio>
+
+static char const *const format = "bytes %" PRId64 "-%" PRId64 "/%" PRId64;
+
+bool
+ContentRange::fromStringClosed(char const *const valstr)
+{
+  int const fields = sscanf(valstr, format, &m_beg, &m_end, &m_length);
+
+  if (3 == fields && m_beg <= m_end) {
+    m_end += 1;
+  } else {
+    m_beg = m_end = m_length = -1;
+  }
+
+  return isValid();
+}
+
+bool
+ContentRange::toStringClosed(char *const rangestr, int *const rangelen) const
+{
+  if (!isValid()) {
+    if (0 < *rangelen) {
+      rangestr[0] = '\0';
+    }
+    *rangelen = 0;
+    return false;
+  }
+
+  int const lenin = *rangelen;
+  *rangelen       = snprintf(rangestr, lenin, format, m_beg, (m_end - 1), m_length);
+
+  return (0 < *rangelen && *rangelen < lenin);
+}
diff --git a/plugins/experimental/slice/ContentRange.h b/plugins/experimental/slice/ContentRange.h
new file mode 100644
index 0000000..2412e66
--- /dev/null
+++ b/plugins/experimental/slice/ContentRange.h
@@ -0,0 +1,53 @@
+/** @file
+  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"
+
+/**
+  represents value parsed from a blocked Content-Range response header field.
+  Range is converted from closed range into a half open range for.
+ */
+struct ContentRange {
+  int64_t m_beg    = -1;
+  int64_t m_end    = -1; // half open
+  int64_t m_length = -1; // full content length
+
+  ContentRange() {}
+  explicit ContentRange(int64_t const begin, int64_t const end, int64_t const len) : m_beg(begin), m_end(end), m_length(len) {}
+  bool
+  isValid() const
+  {
+    return 0 <= m_beg && m_beg < m_end && m_end <= m_length;
+  }
+
+  /** parsed from a Content-Range field
+   */
+  bool fromStringClosed(char const *const valstr);
+
+  /** usable for Content-Range field
+   */
+  bool toStringClosed(char *const rangestr, int *const rangelen) const;
+
+  int64_t
+  rangeSize() const
+  {
+    return m_end - m_beg;
+  }
+};
diff --git a/plugins/experimental/slice/Data.cc b/plugins/experimental/slice/Data.cc
new file mode 100644
index 0000000..aac6943
--- /dev/null
+++ b/plugins/experimental/slice/Data.cc
@@ -0,0 +1,65 @@
+/** @file
+  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 "Data.h"
+
+#include <cassert>
+#include <chrono>
+#include <iostream>
+#include <mutex>
+#include <thread>
+
+namespace
+{
+std::mutex mutex;
+int64_t inplay = 0;
+std::unique_ptr<std::thread> thread;
+} // namespace
+
+void
+monitor()
+{
+  std::lock_guard<std::mutex> guard(mutex);
+  //	while (0 < inplay)
+  while (true) {
+    mutex.unlock();
+    std::this_thread::sleep_for(std::chrono::seconds(10));
+    std::cerr << "Inplay: " << inplay << std::endl;
+    mutex.lock();
+  }
+  //	thread.release();
+}
+
+void
+incrData()
+{
+  std::lock_guard<std::mutex> const guard(mutex);
+  if (!thread) {
+    thread.reset(new std::thread(monitor));
+  }
+
+  ++inplay;
+}
+
+void
+decrData()
+{
+  std::lock_guard<std::mutex> const guard(mutex);
+  --inplay;
+  assert(0 <= inplay);
+}
diff --git a/plugins/experimental/slice/Data.h b/plugins/experimental/slice/Data.h
new file mode 100644
index 0000000..3150bd2
--- /dev/null
+++ b/plugins/experimental/slice/Data.h
@@ -0,0 +1,124 @@
+/** @file
+  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 "HttpHeader.h"
+#include "Range.h"
+#include "Stage.h"
+
+#include <netinet/in.h>
+
+struct Config;
+
+struct Data {
+  Data(Data const &) = delete;
+  Data &operator=(Data const &) = delete;
+
+  Config *const m_config;
+
+  sockaddr_storage m_client_ip;
+
+  // for pristine/effective url coming in
+  TSMBuffer m_urlbuf{nullptr};
+  TSMLoc m_urlloc{nullptr};
+
+  char m_hostname[8192];
+  int m_hostlen;
+  char m_etag[8192];
+  int m_etaglen;
+  char m_lastmodified[8192];
+  int m_lastmodifiedlen;
+
+  TSHttpStatus m_statustype; // 200 or 206
+
+  bool m_bail; // non 206/200 response
+
+  Range m_req_range; // converted to half open interval
+  int64_t m_contentlen;
+
+  int64_t m_blocknum;      // block number to work on, -1 bad/stop
+  int64_t m_blockexpected; // body bytes expected
+  int64_t m_blockskip;     // number of bytes to skip in this block
+  int64_t m_blockconsumed; // body bytes consumed
+
+  enum BlockState { Pending, Active, Done, Fail };
+  BlockState m_blockstate; // is there an active slice block
+
+  int64_t m_bytestosend; // header + content bytes to send
+  int64_t m_bytessent;   // number of bytes written to the client
+
+  bool m_server_block_header_parsed;
+  bool m_server_first_header_parsed;
+
+  Stage m_upstream;
+  Stage m_dnstream;
+
+  HdrMgr m_req_hdrmgr;  // manager for server request
+  HdrMgr m_resp_hdrmgr; // manager for client response
+
+  TSHttpParser m_http_parser{nullptr}; //!< cached for reuse
+
+  explicit Data(Config *const config)
+    : m_config(config),
+      m_client_ip(),
+      m_urlbuf(nullptr),
+      m_urlloc(nullptr),
+      m_hostlen(0),
+      m_etaglen(0),
+      m_lastmodifiedlen(0),
+      m_statustype(TS_HTTP_STATUS_NONE),
+      m_req_range(-1, -1),
+      m_contentlen(-1),
+      m_blocknum(-1),
+      m_blockexpected(0),
+      m_blockskip(0),
+      m_blockconsumed(0),
+      m_blockstate(Pending),
+      m_bytestosend(0),
+      m_bytessent(0),
+      m_server_block_header_parsed(false),
+      m_server_first_header_parsed(false),
+      m_http_parser(nullptr)
+  {
+    m_hostname[0]     = '\0';
+    m_lastmodified[0] = '\0';
+    m_etag[0]         = '\0';
+#if defined(COLLECT_STATS)
+    TSStatIntIncrement(stats::DataCreate, 1);
+#endif
+  }
+
+  ~Data()
+  {
+#if defined(COLLECT_STATS)
+    TSStatIntIncrement(stats::DataDestroy, 1);
+#endif
+    if (nullptr != m_urlbuf) {
+      if (nullptr != m_urlloc) {
+        TSHandleMLocRelease(m_urlbuf, TS_NULL_MLOC, m_urlloc);
+      }
+      TSMBufferDestroy(m_urlbuf);
+    }
+    if (nullptr != m_http_parser) {
+      TSHttpParserDestroy(m_http_parser);
+    }
+  }
+};
diff --git a/plugins/experimental/slice/HttpHeader.cc b/plugins/experimental/slice/HttpHeader.cc
new file mode 100644
index 0000000..9913701
--- /dev/null
+++ b/plugins/experimental/slice/HttpHeader.cc
@@ -0,0 +1,363 @@
+/** @file
+  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 "HttpHeader.h"
+
+#include "slice.h"
+
+#include <cstdlib>
+#include <cstring>
+
+TSHttpType
+HttpHeader::type() const
+{
+  if (isValid()) {
+    return TSHttpHdrTypeGet(m_buffer, m_lochdr);
+  } else {
+    return TS_HTTP_TYPE_UNKNOWN;
+  }
+}
+
+TSHttpStatus
+HttpHeader::status() const
+{
+  TSHttpStatus res = TS_HTTP_STATUS_NONE;
+  if (isValid()) {
+    res = TSHttpHdrStatusGet(m_buffer, m_lochdr);
+  }
+  return res;
+}
+
+bool
+HttpHeader::setStatus(TSHttpStatus const newstatus)
+{
+  if (!isValid()) {
+    return false;
+  }
+
+  return TS_SUCCESS == TSHttpHdrStatusSet(m_buffer, m_lochdr, newstatus);
+}
+
+char *
+HttpHeader ::urlString(int *const urllen) const
+{
+  char *urlstr = nullptr;
+  TSAssert(nullptr != urllen);
+
+  TSMLoc locurl            = nullptr;
+  TSReturnCode const rcode = TSHttpHdrUrlGet(m_buffer, m_lochdr, &locurl);
+  if (TS_SUCCESS == rcode && nullptr != locurl) {
+    urlstr = TSUrlStringGet(m_buffer, locurl, urllen);
+    TSHandleMLocRelease(m_buffer, m_lochdr, locurl);
+  } else {
+    *urllen = 0;
+  }
+
+  return urlstr;
+}
+
+bool
+HttpHeader::setUrl(TSMBuffer const bufurl, TSMLoc const locurl)
+{
+  if (!isValid()) {
+    return false;
+  }
+
+  TSMLoc locurlout   = nullptr;
+  TSReturnCode rcode = TSHttpHdrUrlGet(m_buffer, m_lochdr, &locurlout);
+  if (TS_SUCCESS != rcode) {
+    return false;
+  }
+
+  // copy the url
+  rcode = TSUrlCopy(m_buffer, locurlout, bufurl, locurl);
+
+  // set url active
+  if (TS_SUCCESS == rcode) {
+    rcode = TSHttpHdrUrlSet(m_buffer, m_lochdr, locurlout);
+  }
+
+  TSHandleMLocRelease(m_buffer, m_lochdr, locurlout);
+
+  return TS_SUCCESS == rcode;
+}
+
+bool
+HttpHeader::setReason(char const *const valstr, int const vallen)
+{
+  if (isValid()) {
+    return TS_SUCCESS == TSHttpHdrReasonSet(m_buffer, m_lochdr, valstr, vallen);
+  } else {
+    return false;
+  }
+}
+
+char const *
+HttpHeader::getCharPtr(CharPtrGetFunc func, int *const len) const
+{
+  char const *res = nullptr;
+  if (isValid()) {
+    int reslen = 0;
+    res        = func(m_buffer, m_lochdr, &reslen);
+
+    if (nullptr != len) {
+      *len = reslen;
+    }
+  }
+
+  if (nullptr == res && nullptr != len) {
+    *len = 0;
+  }
+
+  return res;
+}
+
+bool
+HttpHeader::hasKey(char const *const key, int const keylen) const
+{
+  if (!isValid()) {
+    return false;
+  }
+
+  TSMLoc const locfield(TSMimeHdrFieldFind(m_buffer, m_lochdr, key, keylen));
+  if (nullptr != locfield) {
+    TSHandleMLocRelease(m_buffer, m_lochdr, locfield);
+    return true;
+  }
+
+  return false;
+}
+
+bool
+HttpHeader::removeKey(char const *const keystr, int const keylen)
+{
+  if (!isValid()) {
+    return false;
+  }
+
+  bool status = true;
+
+  TSMLoc const locfield = TSMimeHdrFieldFind(m_buffer, m_lochdr, keystr, keylen);
+  if (nullptr != locfield) {
+    int const rcode = TSMimeHdrFieldRemove(m_buffer, m_lochdr, locfield);
+    status          = (TS_SUCCESS == rcode);
+    TSHandleMLocRelease(m_buffer, m_lochdr, locfield);
+  }
+
+  return status;
+}
+
+bool
+HttpHeader::valueForKey(char const *const keystr, int const keylen, char *const valstr, int *const vallen, int const index) const
+{
+  if (!isValid()) {
+    *vallen = 0;
+    return false;
+  }
+
+  bool status = false;
+
+  TSMLoc const locfield = TSMimeHdrFieldFind(m_buffer, m_lochdr, keystr, keylen);
+
+  if (nullptr != locfield) {
+    int getlen               = 0;
+    char const *const getstr = TSMimeHdrFieldValueStringGet(m_buffer, m_lochdr, locfield, index, &getlen);
+
+    int const valcap = *vallen;
+    if (nullptr != getstr && 0 < getlen && getlen < (valcap - 1)) {
+      char *const endp = stpncpy(valstr, getstr, getlen);
+
+      *vallen = endp - valstr;
+      status  = (*vallen < valcap);
+
+      if (status) {
+        *endp = '\0';
+      }
+    }
+    TSHandleMLocRelease(m_buffer, m_lochdr, locfield);
+  } else {
+    *vallen = 0;
+  }
+
+  return status;
+}
+
+bool
+HttpHeader::setKeyVal(char const *const keystr, int const keylen, char const *const valstr, int const vallen, int const index)
+{
+  if (!isValid()) {
+    return false;
+  }
+
+  bool status(false);
+
+  TSMLoc locfield(TSMimeHdrFieldFind(m_buffer, m_lochdr, keystr, keylen));
+
+  if (nullptr != locfield) {
+    status = TS_SUCCESS == TSMimeHdrFieldValueStringSet(m_buffer, m_lochdr, locfield, index, valstr, vallen);
+  } else {
+    int rcode = TSMimeHdrFieldCreateNamed(m_buffer, m_lochdr, keystr, keylen, &locfield);
+
+    if (TS_SUCCESS == rcode) {
+      rcode = TSMimeHdrFieldValueStringSet(m_buffer, m_lochdr, locfield, index, valstr, vallen);
+      if (TS_SUCCESS == rcode) {
+        rcode  = TSMimeHdrFieldAppend(m_buffer, m_lochdr, locfield);
+        status = (TS_SUCCESS == rcode);
+      }
+    }
+  }
+
+  if (nullptr != locfield) {
+    TSHandleMLocRelease(m_buffer, m_lochdr, locfield);
+  }
+
+  return status;
+}
+
+std::string
+HttpHeader::toString() const
+{
+  std::string res;
+
+  if (!isValid()) {
+    return "<null>";
+  }
+
+  TSHttpType const htype(type());
+
+  switch (htype) {
+  case TS_HTTP_TYPE_REQUEST: {
+    res.append(method());
+
+    int urllen         = 0;
+    char *const urlstr = urlString(&urllen);
+    if (nullptr != urlstr) {
+      res.append(" ");
+      res.append(urlstr, urllen);
+      TSfree(urlstr);
+    } else {
+      res.append(" UnknownURL");
+    }
+
+    res.append(" HTTP/unparsed");
+  } break;
+
+  case TS_HTTP_TYPE_RESPONSE: {
+    char bufstr[1024];
+    /*
+    int const version = TSHttpHdrVersionGet(m_buffer, m_lochdr);
+    snprintf(bufstr, 1023, "%d ", version);
+    res.append(bufstr);
+    */
+    res.append("HTTP/unparsed");
+
+    int const status = TSHttpHdrStatusGet(m_buffer, m_lochdr);
+    snprintf(bufstr, 1023, " %d ", status);
+    res.append(bufstr);
+
+    int reasonlen             = 0;
+    char const *const hreason = reason(&reasonlen);
+
+    res.append(hreason, reasonlen);
+  } break;
+
+  default:
+  case TS_HTTP_TYPE_UNKNOWN:
+    res.append("UNKNOWN");
+    break;
+  }
+
+  res.append("\r\n");
+
+  int const numhdrs = TSMimeHdrFieldsCount(m_buffer, m_lochdr);
+
+  for (int indexhdr = 0; indexhdr < numhdrs; ++indexhdr) {
+    TSMLoc const locfield = TSMimeHdrFieldGet(m_buffer, m_lochdr, indexhdr);
+
+    int keylen               = 0;
+    char const *const keystr = TSMimeHdrFieldNameGet(m_buffer, m_lochdr, locfield, &keylen);
+
+    res.append(keystr, keylen);
+    res.append(": ");
+    int vallen               = 0;
+    char const *const valstr = TSMimeHdrFieldValueStringGet(m_buffer, m_lochdr, locfield, -1, &vallen);
+
+    res.append(valstr, vallen);
+    res.append("\r\n");
+
+    TSHandleMLocRelease(m_buffer, m_lochdr, locfield);
+  }
+
+  res.append("\r\n");
+
+  return res;
+}
+
+/////// HdrMgr
+
+TSParseResult
+HdrMgr::populateFrom(TSHttpParser const http_parser, TSIOBufferReader const reader, HeaderParseFunc const parsefunc,
+                     int64_t *const bytes)
+{
+  TSParseResult parse_res = TS_PARSE_CONT;
+
+  if (nullptr == m_buffer) {
+    m_buffer = TSMBufferCreate();
+  }
+  if (nullptr == m_lochdr) {
+    m_lochdr = TSHttpHdrCreate(m_buffer);
+  }
+
+  int64_t avail = TSIOBufferReaderAvail(reader);
+  if (0 < avail) {
+    TSIOBufferBlock block = TSIOBufferReaderStart(reader);
+    int64_t consumed      = 0;
+
+    parse_res = TS_PARSE_CONT;
+
+    while (nullptr != block && 0 < avail) {
+      int64_t blockbytes       = 0;
+      char const *const bstart = TSIOBufferBlockReadStart(block, reader, &blockbytes);
+
+      char const *ptr    = bstart;
+      char const *endptr = ptr + blockbytes;
+
+      parse_res = parsefunc(http_parser, m_buffer, m_lochdr, &ptr, endptr);
+
+      int64_t const bytes_parsed(ptr - bstart);
+
+      consumed += bytes_parsed;
+      avail -= bytes_parsed;
+
+      if (TS_PARSE_CONT == parse_res) {
+        block = TSIOBufferBlockNext(block);
+      } else {
+        break;
+      }
+    }
+    TSIOBufferReaderConsume(reader, consumed);
+
+    if (nullptr != bytes) {
+      *bytes = consumed;
+    }
+  } else if (nullptr != bytes) {
+    *bytes = 0;
+  }
+
+  return parse_res;
+}
diff --git a/plugins/experimental/slice/HttpHeader.h b/plugins/experimental/slice/HttpHeader.h
new file mode 100644
index 0000000..fa3f6cf
--- /dev/null
+++ b/plugins/experimental/slice/HttpHeader.h
@@ -0,0 +1,226 @@
+/** @file
+  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
+
+/**
+ An ATS Http header exists in a marshall buffer at a given location.
+ Unfortunately how that marshall buffer is created and how that
+ location is determined depends on where those buffers came from.
+
+ A TSHttpTxn manages the buffer itself and creates a location which
+ has to be managed.
+
+ A TSHttpParsed populates a created buffer that has had TSHttpHdrCreate
+ run against it which creates a location against it.  End users
+ need to manage the created buffer, the location and invoke
+ TSHttpHdrDestroy.
+*/
+
+#include "ts/ts.h"
+
+#include <string>
+
+static char const *const SLICER_MIME_FIELD_INFO = "X-Slicer-Info";
+
+/**
+  Designed to be a cheap throwaway struct which allows a
+  consumer to make various calls to manipulate headers.
+*/
+struct HttpHeader {
+  TSMBuffer const m_buffer;
+  TSMLoc const m_lochdr;
+
+  explicit HttpHeader(TSMBuffer buffer, TSMLoc lochdr) : m_buffer(buffer), m_lochdr(lochdr) {}
+  bool
+  isValid() const
+  {
+    return nullptr != m_buffer && nullptr != m_lochdr;
+  }
+
+  int
+  byteSize() const
+  {
+    if (isValid()) {
+      return TSHttpHdrLengthGet(m_buffer, m_lochdr);
+    } else {
+      return 0;
+    }
+  }
+
+  // TS_HTTP_TYPE_UNKNOWN, TS_HTTP_TYPE_REQUEST, TS_HTTP_TYPE_RESPONSE
+  TSHttpType type() const;
+
+  TSHttpStatus status() const;
+
+  bool setStatus(TSHttpStatus const newstatus);
+
+  bool setUrl(TSMBuffer const bufurl, TSMLoc const locurl);
+
+  typedef char const *(*CharPtrGetFunc)(TSMBuffer, TSMLoc, int *);
+
+  // request method TS_HTTP_METHOD_*
+  char const *
+  method(int *const len = nullptr) const
+  {
+    return getCharPtr(TSHttpHdrMethodGet, len);
+  }
+
+  // request method version
+  int
+  version() const
+  {
+    return TSHttpHdrVersionGet(m_buffer, m_lochdr);
+  }
+
+  // Returns string representation of the url. Caller gets ownership!
+  char *urlString(int *const urllen) const;
+
+  // host
+  char const *
+  hostname(int *const len) const
+  {
+    return getCharPtr(TSHttpHdrHostGet, len);
+  }
+
+  // response reason
+  char const *
+  reason(int *const len) const
+  {
+    return getCharPtr(TSHttpHdrReasonGet, len);
+  }
+
+  bool setReason(char const *const valstr, int const vallen);
+
+  bool hasKey(char const *const key, int const keylen) const;
+
+  // returns false if header invalid or something went wrong with removal.
+  bool removeKey(char const *const key, int const keylen);
+
+  bool valueForKey(char const *const keystr, int const keylen,
+                   char *const valstr,  // <-- return string value
+                   int *const vallen,   // <-- pass in capacity, returns len of string
+                   int const index = -1 // retrieves all values
+                   ) const;
+
+  /**
+    Sets or adds a key/value
+  */
+  bool setKeyVal(char const *const key, int const keylen, char const *const val, int const vallen,
+                 int const index = -1 // sets all values
+  );
+
+  /** dump header into provided char buffer
+   */
+  std::string toString() const;
+
+private:
+  /**
+    To be used with
+    TSHttpHdrMethodGet
+    TSHttpHdrHostGet
+    TSHttpHdrReasonGet
+   */
+  char const *getCharPtr(CharPtrGetFunc func, int *const len) const;
+};
+
+struct TxnHdrMgr {
+  TxnHdrMgr(TxnHdrMgr const &) = delete;
+  TxnHdrMgr &operator=(TxnHdrMgr const &) = delete;
+
+  TSMBuffer m_buffer{nullptr};
+  TSMLoc m_lochdr{nullptr};
+
+  TxnHdrMgr() : m_buffer(nullptr), m_lochdr(nullptr) {}
+  ~TxnHdrMgr()
+  {
+    if (nullptr != m_lochdr) {
+      TSHandleMLocRelease(m_buffer, TS_NULL_MLOC, m_lochdr);
+    }
+  }
+
+  typedef TSReturnCode (*HeaderGetFunc)(TSHttpTxn, TSMBuffer *, TSMLoc *);
+  /** use one of the following:
+    TSHttpTxnClientReqGet
+    TSHttpTxnClientRespGet
+    TSHttpTxnServerReqGet
+    TSHttpTxnServerRespGet
+    TSHttpTxnCachedReqGet
+    TSHttpTxnCachedRespGet
+  */
+
+  bool
+  populateFrom(TSHttpTxn const &txnp, HeaderGetFunc const &func)
+  {
+    return TS_SUCCESS == func(txnp, &m_buffer, &m_lochdr);
+  }
+
+  bool
+  isValid() const
+  {
+    return nullptr != m_lochdr;
+  }
+};
+
+struct HdrMgr {
+  HdrMgr(HdrMgr const &) = delete;
+  HdrMgr &operator=(HdrMgr const &) = delete;
+
+  TSMBuffer m_buffer{nullptr};
+  TSMLoc m_lochdr{nullptr};
+
+  HdrMgr() : m_buffer(nullptr), m_lochdr(nullptr) {}
+  ~HdrMgr()
+  {
+    if (nullptr != m_buffer) {
+      if (nullptr != m_lochdr) {
+        TSHttpHdrDestroy(m_buffer, m_lochdr);
+        TSHandleMLocRelease(m_buffer, TS_NULL_MLOC, m_lochdr);
+      }
+      TSMBufferDestroy(m_buffer);
+    }
+  }
+
+  void
+  resetHeader()
+  {
+    if (nullptr != m_buffer && nullptr != m_lochdr) {
+      TSHttpHdrDestroy(m_buffer, m_lochdr);
+      TSHandleMLocRelease(m_buffer, TS_NULL_MLOC, m_lochdr);
+      m_lochdr = nullptr;
+    }
+  }
+
+  typedef TSParseResult (*HeaderParseFunc)(TSHttpParser, TSMBuffer, TSMLoc, char const **, char const *);
+
+  /** Clear/create the parser before calling this and don't
+   use the parser on another header until done with this one.
+   use one of the following:
+     TSHttpHdrParseReq
+     TSHttpHdrParseResp
+    Call this multiple times if necessary.
+  */
+  TSParseResult populateFrom(TSHttpParser const http_parser, TSIOBufferReader const reader, HeaderParseFunc const parsefunc,
+                             int64_t *const consumed);
+
+  bool
+  isValid() const
+  {
+    return nullptr != m_lochdr;
+  }
+};
diff --git a/plugins/experimental/slice/Makefile.inc b/plugins/experimental/slice/Makefile.inc
new file mode 100644
index 0000000..21f6166
--- /dev/null
+++ b/plugins/experimental/slice/Makefile.inc
@@ -0,0 +1,64 @@
+#  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.
+
+pkglib_LTLIBRARIES += experimental/slice/slice.la
+
+experimental_slice_slice_la_SOURCES = \
+  experimental/slice/client.cc \
+  experimental/slice/client.h \
+  experimental/slice/Config.cc \
+  experimental/slice/Config.h \
+  experimental/slice/ContentRange.cc \
+  experimental/slice/ContentRange.h \
+  experimental/slice/Data.h \
+  experimental/slice/HttpHeader.cc \
+  experimental/slice/HttpHeader.h \
+  experimental/slice/intercept.cc \
+  experimental/slice/intercept.h \
+  experimental/slice/Range.cc \
+  experimental/slice/Range.h \
+  experimental/slice/response.cc \
+  experimental/slice/response.h \
+  experimental/slice/server.cc \
+  experimental/slice/server.h \
+  experimental/slice/slice.cc \
+  experimental/slice/slice.h \
+  experimental/slice/Stage.h \
+  experimental/slice/transfer.cc \
+  experimental/slice/transfer.h \
+  experimental/slice/util.cc \
+  experimental/slice/util.h
+
+check_PROGRAMS += experimental/slice/test_content_range
+
+experimental_slice_test_content_range_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DUNITTEST
+experimental_slice_test_content_range_SOURCES = \
+  experimental/slice/unit-tests/test_content_range.cc \
+  experimental/slice/ContentRange.cc
+
+check_PROGRAMS += experimental/slice/test_range
+
+experimental_slice_test_range_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DUNITTEST
+experimental_slice_test_range_SOURCES = \
+  experimental/slice/unit-tests/test_range.cc \
+  experimental/slice/Range.cc
+
+check_PROGRAMS += experimental/slice/test_config
+
+experimental_slice_test_config_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DUNITTEST
+experimental_slice_test_config_SOURCES = \
+  experimental/slice/unit-tests/test_config.cc \
+  experimental/slice/Config.cc
diff --git a/plugins/experimental/slice/Makefile.tsxs b/plugins/experimental/slice/Makefile.tsxs
new file mode 100644
index 0000000..f8022a1
--- /dev/null
+++ b/plugins/experimental/slice/Makefile.tsxs
@@ -0,0 +1,65 @@
+#  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.
+
+TSXS?=tsxs
+PLUGIN=slice
+
+all: $(PLUGIN).so
+
+SOURCES = \
+	Config.cc \
+	ContentRange.cc \
+	HttpHeader.cc \
+	Range.cc \
+	client.cc \
+	intercept.cc \
+	response.cc \
+	server.cc \
+	slice.cc \
+	transfer.cc \
+
+HEADERS = \
+	Config.h \
+	ContentRange.h \
+	Data.h \
+	HttpHeader.h \
+	Range.h \
+	Stage.h \
+	client.h \
+	intercept.h \
+	response.h \
+	server.h \
+	slice.h \
+	transfer.h \
+
+$(PLUGIN).so: $(SOURCES) $(HEADERS)
+	$(TSXS) -v -o $(PLUGIN).so $(SOURCES)
+
+install: all
+	$(TSXS) -v -o $(PLUGIN).so -i
+
+TSINCLUDE = $(shell tsxs -q INCLUDEDIR)
+TSCXX = $(shell tsxs -q CXX)
+TSCXXFLAGS = $(shell tsxs -q CXXFLAGS)
+#PREFIX = $(shell tsxs -q PREFIX)
+#LIBS = -L$(PREFIX)/lib -latscppapi
+#LIBS = $(PREFIX)/lib/libtsutil.la
+
+slice_test: slice_test.cc ContentRange.cc Range.cc
+	$(TSCXX) -o $@ $^ $(TSCXXFLAGS) -I$(TSINCLUDE) -DUNITTEST
+
+clean:
+	rm -fv *.lo *.so
diff --git a/plugins/experimental/slice/README.md b/plugins/experimental/slice/README.md
new file mode 100644
index 0000000..6ae1399
--- /dev/null
+++ b/plugins/experimental/slice/README.md
@@ -0,0 +1,165 @@
+### Apache Traffic Server - Slicer Plugin
+
+The purpose of this plugin is to slice full file or range based requests
+into deterministic chunks.  This allows a large file to be spread across
+multiple cache stripes and allows range requests to be satisfied by
+stitching these chunks together.
+
+Deterministic chunks are requested from a parent cache or origin server
+using a preconfigured block byte size.
+
+The plugin is an example of an intercept handler which takes a single
+incoming request (range or whole asset), breaks it into a sequence
+of block requests and assembles those blocks into a client response.
+The plugin uses TSHttpConnect to delegate each block request to
+cache_range_requests.so which handles all cache and parent interaction.
+
+To enable the plugin, specify the plugin library via @plugin at the end
+of a remap line as follows (default 1MB slice in this example):
+
+```
+map http://ats-cache/ http://parent/ @plugin=slice.so @plugin=cache_range_requests.so
+map https://ats-cache/ http://parent/ @plugin=slice.so @plugin=cache_range_requests.so
+```
+
+alternatively (2MB slice block)
+
+```
+map http://ats-cache/ http://parent/ @plugin=slice.so @pparam=-b @pparam=2M @plugin=cache_range_requests.so
+map https://ats-cache/ http://parent/ @plugin=slice.so @pparam=--blockbytes=2M @plugin=cache_range_requests.so
+```
+
+Options for the slice plugin (typically last one wins):
+```
+--blockbytes=<number bytes> (optional)
+  Slice block size.
+  Default is 1m or 1048576 bytes.
+  also -b <num bytes>
+  Suffix k,m,g supported.
+  Limited to 32k and 32m inclusive.
+  For backwards compatibility blockbytes:<num bytes> is also supported.
+
+--blockbytes-test=<number bytes> (optional)
+  Slice block size for testing.
+  also -t <num bytes>
+  Suffix k,m,g supported.
+  Limited to any positive number.
+  Ignored if --blockbytes is provided.
+
+--remap-host=<loopback hostname> (optional)
+  Uses effective url with given host and port 0 for remapping.
+  Requires setting up an intermediate loopback remap rule.
+  -r for short
+
+--pace-errorlog=<second(s)> (optional)
+  Limit stitching error logs to every 'n' second(s)
+  Default is to log all errors (no pacing).
+  also -e <seconds>
+
+--disable-errorlog (optional)
+  Disable writing stitching errors to the error log.
+  also -d
+```
+
+By default the plugin uses the pristine url to loopback call back
+into the same rule as each range slice is issued.  The effective url
+with loopback remap host may be used by adding the '-r <hostname>' or
+'--remap-host=<hostname>' plugin option.
+
+Using the `--remap-host` option splits the plugin chain into 2 remap rules.
+One remap rule for all the incoming requests and the other for just the block
+range requests.  This allows for easier trouble shooting via logs and
+also allows for more effecient plugin rules.  The default pristine method
+runs the remap plugins twice, one for the incoming request and one for
+eace slice.  Splitting the rules allows for plugins like URI signing to
+be done on the client request only.
+
+NOTE: Requests NOT handled by the slice plugin (ie: HEAD requests) are
+handled as with a typical remap rule.  GET requests intercepted by the
+slice plugin are virtually reissued into ATS and are forward proxied
+through the cache_range_requests plugin.
+
+```
+map http://ats/ http://parent/ @plugin=slice.so @pparam=--blockbytes=512k @pparam=--remap-host=loopback
+map https://ats/ https://parent/ @plugin=slice.so @pparam=--blockbytes=512k @pparam=--remap-host=loopback
+
+# Virtual forward proxy for slice range blocks
+map http://loopback/ http://parent/ @plugin=cache_range_requests.so
+map https://loopback/ http://parent/ @plugin=cache_range_requests.so
+```
+
+**Note**: For default pristine behavior cache_range_requests **MUST**
+follow slice.so Put these plugins at the end of the plugin list
+
+**Note**: blockbytes is defined in bytes. Postfix for 'K', 'M' and 'G'
+may be used.  1048576 (1MB) is the default.
+
+For testing purposes an unchecked value of "blockbytes-test" is also available.
+
+Debug output can be enable by setting the debug tag: **slice**.  If debug
+is enabled all block stitch errors will log to diags.log
+
+The slice plugin is susceptible to block stitching errors caused by
+mismatched blocks.  For these cases special detailed error logs are
+provided to help with debugging.  Below is a sample error log entry::
+
+```
+[Apr 19 20:26:13.639] [ET_NET 17] ERROR: [slice] 1555705573.639 reason="Non 206 internal block response" uri="http://localhost:18080/%7Ep.tex/%7Es.50M/%7Eui.20000/" uas="curl/7.29.0" req_range="bytes=1000000-" norm_range="bytes 1000000-52428799/52428800" etag_exp="%221603934496%22" lm_exp="Fri, 19 Apr 2019 18:53:20 GMT" blk_range="21000000-21999999" status_got="400" cr_got="" etag_got="" lm_got="" cc="no-store" via=""
+```
+
+Current error types logged:
+```
+    Mismatch block Etag
+    Mismatch block Last-Modified
+    Non 206 internal block response
+    Mismatch/Bad block Content-Range
+```
+
+
+With slice error logs disabled these type errors can typically be detected
+by observing crc=ERR_READ_ERROR and pscl=0 in normal logs.
+
+At the current time only single range requests or the first part of a
+multi part range request of the forms:
+```
+Range: bytes=<begin>-<end>
+Range: bytes=<begin>-
+Range: bytes=-<last N bytes>
+```
+are supported as multi part range responses are non trivial to implement.
+This matches with the cache_range_requests.so plugin capability.
+---
+
+Important things to note:
+
+Any first block server response that is not a 206 is passed down to
+the client.
+
+Only the first server response block is used to evaluate any "If-"
+headers.  Subsequent server slice block requests remove these headers.
+
+If a client aborts mid transaction the current slice block is completed
+to ensure that the block is written to cache.
+
+The only 416 case this plugin handles itself is if the requested range
+is inside the end slice block but past the content length.  Otherwise
+parents seem to properly issue 416 responses themselves.
+
+---
+
+To manually build the plugin use the "tsxs" executable that installs with
+traffic_server.
+
+Running the following command will build the plugin
+
+```
+tsxs -v -o slice.so *.cc
+```
+
+Running the following command will build and install the plugin.
+Beware this may crash a running system if the plugin is loaded
+and the OS uses memory paging with plugins.
+
+```
+tsxs -v -i -o slice.so *.cc
+```
diff --git a/plugins/experimental/slice/Range.cc b/plugins/experimental/slice/Range.cc
new file mode 100644
index 0000000..b52e8b6
--- /dev/null
+++ b/plugins/experimental/slice/Range.cc
@@ -0,0 +1,196 @@
+/** @file
+  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 "Range.h"
+#include "slice.h"
+
+#include <algorithm>
+#include <cinttypes>
+#include <cctype>
+#include <cstdio>
+#include <cstring>
+
+bool
+Range::isValid() const
+{
+  return m_beg < m_end && (0 <= m_beg || 0 == m_end);
+}
+
+int64_t
+Range::size() const
+{
+  return m_end - m_beg;
+}
+
+bool
+Range::fromStringClosed(char const *const rangestr)
+{
+  static char const *const BYTESTR = "bytes=";
+  static size_t const BYTESTRLEN   = strlen(BYTESTR);
+
+  m_beg = m_end = -1; // initialize invalid
+
+  // make sure this is in byte units
+  if (0 != strncmp(BYTESTR, rangestr, BYTESTRLEN)) {
+    return false;
+  }
+
+  // advance past any white space
+  char const *pstr = rangestr + BYTESTRLEN;
+  while ('\0' != *pstr && isblank(*pstr)) {
+    ++pstr;
+  }
+
+  // rip out any whitespace
+  char rangebuf[1024];
+  int const rangelen = sizeof(rangebuf);
+  char *pbuf         = rangebuf;
+  while ('\0' != *pstr && (pbuf - rangebuf) < rangelen) {
+    if (!isblank(*pstr)) {
+      *pbuf++ = *pstr;
+    }
+    ++pstr;
+  }
+  *pbuf = '\0';
+
+  int const rlen = (pbuf - rangebuf);
+
+  int consumed = 0;
+
+  // last 'n' bytes - result in range with negative begin and 0 end
+  int64_t endbytes         = 0;
+  char const *const fmtend = "-%" PRId64 "%n";
+  int const fieldsend      = sscanf(rangebuf, fmtend, &endbytes, &consumed);
+  if (1 == fieldsend) {
+    if (rlen == consumed) {
+      m_beg = -endbytes;
+      m_end = 0;
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  // normal range <front>-<back>
+  char const *const fmtclosed = "%" PRId64 "-%" PRId64 "%n";
+  int64_t front               = 0;
+  int64_t back                = 0;
+
+  int const fieldsclosed = sscanf(rangebuf, fmtclosed, &front, &back, &consumed);
+  if (2 == fieldsclosed) {
+    if (0 <= front && front <= back && rlen == consumed) {
+      m_beg = front;
+      m_end = back + 1;
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  front                    = 0;
+  char const *const fmtbeg = "%" PRId64 "-%n";
+  int const fieldsbeg      = sscanf(rangebuf, fmtbeg, &front, &consumed);
+  if (1 == fieldsbeg) {
+    if (rlen == consumed) {
+      m_beg = front;
+      m_end = Range::maxval;
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  return false;
+} // parseRange
+
+bool
+Range::toStringClosed(char *const bufstr,
+                      int *const buflen // returns actual bytes used
+                      ) const
+{
+  if (!isValid()) {
+    if (0 < *buflen) {
+      bufstr[0] = '\0';
+    }
+    *buflen = 0;
+    return false;
+  }
+
+  int const lenin = *buflen;
+
+  if (m_end <= Range::maxval) {
+    *buflen = snprintf(bufstr, lenin, "bytes=%" PRId64 "-%" PRId64, m_beg, m_end - 1);
+  } else {
+    *buflen = snprintf(bufstr, lenin, "bytes=%" PRId64 "-", m_beg);
+  }
+
+  return (0 < *buflen && *buflen < lenin);
+}
+
+int64_t
+Range::firstBlockFor(int64_t const blocksize) const
+{
+  if (0 < blocksize && isValid()) {
+    return std::max(static_cast<int64_t>(0), m_beg / blocksize);
+  } else {
+    return -1;
+  }
+}
+
+int64_t
+Range::lastBlockFor(int64_t const blocksize) const
+{
+  if (0 < blocksize && isValid()) {
+    return std::max((int64_t)0, (m_end - 1) / blocksize);
+  } else {
+    return -1;
+  }
+}
+
+Range
+Range::intersectedWith(Range const &other) const
+{
+  return Range(std::max(m_beg, other.m_beg), std::min(m_end, other.m_end));
+}
+
+bool
+Range::blockIsInside(int64_t const blocksize, int64_t const blocknum) const
+{
+  Range const blockrange(blocksize * blocknum, blocksize * (blocknum + 1));
+  Range const isec(blockrange.intersectedWith(*this));
+
+  return isec.isValid();
+}
+
+int64_t
+Range::skipBytesForBlock(int64_t const blocksize, int64_t const blocknum) const
+{
+  int64_t const blockstart(blocksize * blocknum);
+
+  if (m_beg < blockstart) {
+    return 0;
+  } else {
+    return m_beg - blockstart;
+  }
+}
+
+bool
+Range::isEndBytes() const
+{
+  return m_beg < 0 && 0 == m_end;
+}
diff --git a/plugins/experimental/slice/Range.h b/plugins/experimental/slice/Range.h
new file mode 100644
index 0000000..8c8826d
--- /dev/null
+++ b/plugins/experimental/slice/Range.h
@@ -0,0 +1,78 @@
+/** @file
+  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 <limits>
+
+/**
+  represents a value parsed from a Range request header field.
+  Range is converted from a closed range into a half open.
+ */
+
+struct Range {
+public:
+  static int64_t constexpr maxval = (std::numeric_limits<int64_t>::max() >> 2);
+
+  int64_t m_beg = -1;
+  int64_t m_end = -1; // half open
+
+  Range() {}
+  explicit Range(int64_t const begin, int64_t const end) : m_beg(begin), m_end(end) {}
+
+  bool isValid() const;
+
+  int64_t size() const;
+
+  /** parse a from a closed request range into a half open range
+   * This will only correctly handle the *first* range that is
+   * parsed via TSMimeHdrFieldValueStringGet with index '0'.
+   * Range representing last N bytes will be coded as (-N, 0)
+   */
+  bool fromStringClosed(char const *const rangestr);
+
+  /** parse a from a closed request range into a half open range
+   */
+  bool toStringClosed(char *const rangestr, int *const rangelen) const;
+
+  /** block number of first range block
+   */
+  int64_t firstBlockFor(int64_t const blockbytes) const;
+
+  /** block number of last (inclusive) range block
+   */
+  int64_t lastBlockFor(int64_t const blockbytes) const;
+
+  /** block intersection
+   */
+  Range intersectedWith(Range const &other) const;
+
+  /** is the given block inside held range?
+   */
+  bool blockIsInside(int64_t const blocksize, int64_t const blocknum) const;
+
+  /** number of skip bytes for the given block
+   */
+  int64_t skipBytesForBlock(int64_t const blocksize, int64_t const blocknum) const;
+
+  /** is this coded to indicate last N bytes?
+   */
+  bool isEndBytes() const;
+};
diff --git a/plugins/experimental/slice/Stage.h b/plugins/experimental/slice/Stage.h
new file mode 100644
index 0000000..f677d64
--- /dev/null
+++ b/plugins/experimental/slice/Stage.h
@@ -0,0 +1,190 @@
+/** @file
+  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 "slice.h"
+#include "util.h"
+
+#include <cinttypes>
+
+struct Channel {
+  TSVIO m_vio{nullptr};
+  TSIOBuffer m_iobuf{nullptr};
+  TSIOBufferReader m_reader{nullptr};
+
+  ~Channel()
+  {
+    if (nullptr != m_reader) {
+      TSIOBufferReaderFree(m_reader);
+#if defined(COLLECT_STATS)
+      TSStatIntDecrement(stats::Reader, 1);
+#endif
+    }
+    if (nullptr != m_iobuf) {
+      TSIOBufferDestroy(m_iobuf);
+    }
+  }
+
+  int64_t
+  drainReader()
+  {
+    int64_t consumed = 0;
+
+    if (nullptr != m_reader && reader_avail_more_than(m_reader, 0)) {
+      int64_t const avail = TSIOBufferReaderAvail(m_reader);
+      TSIOBufferReaderConsume(m_reader, avail);
+      consumed = avail;
+      TSVIONDoneSet(m_vio, TSVIONDoneGet(m_vio) + consumed);
+    }
+
+    return consumed;
+  }
+
+  bool
+  setForRead(TSVConn vc, TSCont contp, int64_t const bytesin)
+  {
+    TSAssert(nullptr != vc);
+    if (nullptr == m_iobuf) {
+      m_iobuf  = TSIOBufferCreate();
+      m_reader = TSIOBufferReaderAlloc(m_iobuf);
+#if defined(COLLECT_STATS)
+      TSStatIntIncrement(stats::Reader, 1);
+#endif
+    } else {
+      int64_t const drained = drainReader();
+      if (0 < drained) {
+        DEBUG_LOG("Drained from reader: %" PRId64, drained);
+      }
+    }
+    m_vio = TSVConnRead(vc, contp, m_iobuf, bytesin);
+    return nullptr != m_vio;
+  }
+
+  bool
+  setForWrite(TSVConn vc, TSCont contp, int64_t const bytesout)
+  {
+    TSAssert(nullptr != vc);
+    if (nullptr == m_iobuf) {
+      m_iobuf  = TSIOBufferCreate();
+      m_reader = TSIOBufferReaderAlloc(m_iobuf);
+#if defined(COLLECT_STATS)
+      TSStatIntIncrement(stats::Reader, 1);
+#endif
+    } else {
+      int64_t const drained = drainReader();
+      if (0 < drained) {
+        DEBUG_LOG("Drained from reader: %" PRId64, drained);
+      }
+    }
+    m_vio = TSVConnWrite(vc, contp, m_reader, bytesout);
+    return nullptr != m_vio;
+  }
+
+  void
+  close()
+  {
+    if (nullptr != m_reader) {
+      drainReader();
+    }
+    m_vio = nullptr;
+  }
+
+  bool
+  isOpen() const
+  {
+    return nullptr != m_vio;
+  }
+
+  bool
+  isDrained() const
+  {
+    return nullptr == m_reader || !reader_avail_more_than(m_reader, 0);
+  }
+};
+
+struct Stage // upstream or downstream (server or client)
+{
+  Stage(Stage const &) = delete;
+  Stage &operator=(Stage const &) = delete;
+
+  TSVConn m_vc{nullptr};
+  Channel m_read;
+  Channel m_write;
+
+  Stage() {}
+  ~Stage()
+  {
+    if (nullptr != m_vc) {
+      TSVConnClose(m_vc);
+    }
+  }
+
+  void
+  setupConnection(TSVConn vc)
+  {
+    if (nullptr != m_vc) {
+      TSVConnClose(m_vc);
+    }
+    m_read.close();
+    m_write.close();
+    m_vc = vc;
+  }
+
+  void
+  setupVioRead(TSCont contp, int64_t const bytesin)
+  {
+    m_read.setForRead(m_vc, contp, bytesin);
+  }
+
+  void
+  setupVioWrite(TSCont contp, int64_t const bytesout)
+  {
+    m_write.setForWrite(m_vc, contp, bytesout);
+  }
+
+  void
+  abort()
+  {
+    if (nullptr != m_vc) {
+      TSVConnAbort(m_vc, TS_VC_CLOSE_ABORT);
+      m_vc = nullptr;
+    }
+    m_read.close();
+    m_write.close();
+  }
+
+  void
+  close()
+  {
+    if (nullptr != m_vc) {
+      TSVConnClose(m_vc);
+      m_vc = nullptr;
+    }
+    m_read.close();
+    m_write.close();
+  }
+
+  bool
+  isOpen() const
+  {
+    return nullptr != m_vc && (m_read.isOpen() || m_write.isOpen());
+  }
+};
diff --git a/plugins/experimental/slice/client.cc b/plugins/experimental/slice/client.cc
new file mode 100644
index 0000000..5d4bde5
--- /dev/null
+++ b/plugins/experimental/slice/client.cc
@@ -0,0 +1,165 @@
+/** @file
+  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 "client.h"
+
+#include "Config.h"
+#include "util.h"
+
+#include <cinttypes>
+
+// this is called once per transaction when the client sends a req header
+bool
+handle_client_req(TSCont contp, TSEvent event, Data *const data)
+{
+  switch (event) {
+  case TS_EVENT_VCONN_READ_READY:
+  case TS_EVENT_VCONN_READ_COMPLETE: {
+    if (nullptr == data->m_http_parser) {
+      data->m_http_parser = TSHttpParserCreate();
+    }
+
+    // Read the header from the buffer
+    int64_t consumed = 0;
+    if (TS_PARSE_DONE !=
+        data->m_req_hdrmgr.populateFrom(data->m_http_parser, data->m_dnstream.m_read.m_reader, TSHttpHdrParseReq, &consumed)) {
+      return false;
+    }
+
+    // update the VIO
+    TSVIO const input_vio = data->m_dnstream.m_read.m_vio;
+    TSVIONDoneSet(input_vio, TSVIONDoneGet(input_vio) + consumed);
+
+    // make the header manipulator
+    HttpHeader header(data->m_req_hdrmgr.m_buffer, data->m_req_hdrmgr.m_lochdr);
+
+    // set the request url back to pristine in case of plugin stacking
+    header.setUrl(data->m_urlbuf, data->m_urlloc);
+
+    header.setKeyVal(TS_MIME_FIELD_HOST, TS_MIME_LEN_HOST, data->m_hostname, data->m_hostlen);
+
+    // default: whole file (unknown, wait for first server response)
+    Range rangebe;
+
+    char rangestr[1024];
+    int rangelen        = sizeof(rangestr);
+    bool const hasRange = header.valueForKey(TS_MIME_FIELD_RANGE, TS_MIME_LEN_RANGE, rangestr, &rangelen,
+                                             0); // <-- first range only
+    if (hasRange) {
+      // write parsed header into slicer meta tag
+      header.setKeyVal(SLICER_MIME_FIELD_INFO, strlen(SLICER_MIME_FIELD_INFO), rangestr, rangelen);
+      bool const isRangeGood = rangebe.fromStringClosed(rangestr);
+
+      if (isRangeGood) {
+        DEBUG_LOG("%p Partial content request", data);
+        data->m_statustype = TS_HTTP_STATUS_PARTIAL_CONTENT;
+      } else // signal a 416 needs to be formed and sent
+      {
+        DEBUG_LOG("%p Ill formed/unhandled range: %s", data, rangestr);
+        data->m_statustype = TS_HTTP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE;
+
+        // First block will give Content-Length
+        rangebe = Range(0, data->m_config->m_blockbytes);
+      }
+    } else {
+      DEBUG_LOG("%p Full content request", data);
+      static char const *const valstr = "-";
+      static size_t const vallen      = strlen(valstr);
+      header.setKeyVal(SLICER_MIME_FIELD_INFO, strlen(SLICER_MIME_FIELD_INFO), valstr, vallen);
+      data->m_statustype = TS_HTTP_STATUS_OK;
+      rangebe            = Range(0, Range::maxval);
+    }
+
+    // set to the first block in range
+    data->m_blocknum  = rangebe.firstBlockFor(data->m_config->m_blockbytes);
+    data->m_req_range = rangebe;
+
+    // remove ATS keys to avoid 404 loop
+    header.removeKey(TS_MIME_FIELD_VIA, TS_MIME_LEN_VIA);
+    header.removeKey(TS_MIME_FIELD_X_FORWARDED_FOR, TS_MIME_LEN_X_FORWARDED_FOR);
+
+    // send the first block request to server
+    if (!request_block(contp, data)) {
+      abort(contp, data);
+      return false;
+    }
+
+    // for subsequent blocks remove any conditionals which may fail
+    // an optimization would be to wait until the first block succeeds
+    header.removeKey(TS_MIME_FIELD_IF_MATCH, TS_MIME_LEN_IF_MATCH);
+    header.removeKey(TS_MIME_FIELD_IF_MODIFIED_SINCE, TS_MIME_LEN_IF_MODIFIED_SINCE);
+    header.removeKey(TS_MIME_FIELD_IF_NONE_MATCH, TS_MIME_LEN_IF_NONE_MATCH);
+    header.removeKey(TS_MIME_FIELD_IF_RANGE, TS_MIME_LEN_IF_RANGE);
+    header.removeKey(TS_MIME_FIELD_IF_UNMODIFIED_SINCE, TS_MIME_LEN_IF_UNMODIFIED_SINCE);
+  } break;
+  default: {
+    DEBUG_LOG("%p handle_client_req unhandled event %d %s", data, event, TSHttpEventNameLookup(event));
+  } break;
+  }
+
+  return true;
+}
+
+// this is when the client starts asking us for more data
+void
+handle_client_resp(TSCont contp, TSEvent event, Data *const data)
+{
+  DEBUG_LOG("%p handle_client_resp %s", data, TSHttpEventNameLookup(event));
+
+#if defined(COLLECT_STATS)
+  TSStatIntIncrement(stats::Client, 1);
+#endif
+
+  switch (event) {
+  case TS_EVENT_VCONN_WRITE_READY: {
+    if (Data::BlockState::Pending == data->m_blockstate) {
+      bool start_next_block = true;
+
+      if (data->m_config->m_throttle) {
+        TSVIO const output_vio    = data->m_dnstream.m_write.m_vio;
+        int64_t const output_done = TSVIONDoneGet(output_vio);
+        int64_t const output_sent = data->m_bytessent;
+        int64_t const threshout   = data->m_config->m_blockbytes;
+
+        if (threshout < (output_done - output_sent)) {
+          start_next_block = false;
+          DEBUG_LOG("%p handle_client_resp: throttling %" PRId64, data, (output_done - output_sent));
+        }
+      }
+
+      if (start_next_block) {
+        request_block(contp, data);
+      }
+    }
+  } break;
+  case TS_EVENT_VCONN_WRITE_COMPLETE: {
+    if (TSIsDebugTagSet(PLUGIN_NAME) && reader_avail_more_than(data->m_upstream.m_read.m_reader, 0)) {
+      int64_t const left = TSIOBufferReaderAvail(data->m_upstream.m_read.m_reader);
+      DEBUG_LOG("%p WRITE_COMPLETE called with %" PRId64 " bytes left", data, left);
+    }
+
+    data->m_dnstream.close();
+    if (!data->m_upstream.m_read.isOpen()) {
+      shutdown(contp, data);
+    }
+  } break;
+  default: {
+    DEBUG_LOG("%p handle_client_resp unhandled event %d %s", data, event, TSHttpEventNameLookup(event));
+  } break;
+  }
+}
diff --git a/plugins/experimental/slice/client.h b/plugins/experimental/slice/client.h
new file mode 100644
index 0000000..b915161
--- /dev/null
+++ b/plugins/experimental/slice/client.h
@@ -0,0 +1,36 @@
+/** @file
+  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 "Data.h"
+
+#include "ts/ts.h"
+
+/** Functions to deal with the connection to the client.
+ * Body content transfers are handled by the client.
+ * New block requests are also initiated by the client.
+ */
+
+bool requestBlock(TSCont contp, Data *const data);
+
+/** returns true if the incoming vio can be turned off
+ */
+bool handle_client_req(TSCont contp, TSEvent event, Data *const data);
+
+void handle_client_resp(TSCont contp, TSEvent event, Data *const data);
diff --git a/plugins/experimental/slice/intercept.cc b/plugins/experimental/slice/intercept.cc
new file mode 100644
index 0000000..2b6ef3b
--- /dev/null
+++ b/plugins/experimental/slice/intercept.cc
@@ -0,0 +1,82 @@
+/** @file
+  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 "intercept.h"
+
+#include "Data.h"
+#include "client.h"
+#include "server.h"
+#include "slice.h"
+
+int
+intercept_hook(TSCont contp, TSEvent event, void *edata)
+{
+  Data *const data = static_cast<Data *>(TSContDataGet(contp));
+
+  if (nullptr == data) {
+    ERROR_LOG("intercept_hook called without data");
+    TSContDestroy(contp);
+    return TS_EVENT_ERROR;
+  }
+
+  // After the initial TS_EVENT_NET_ACCEPT
+  // any "events" will be handled by the vio read or write channel handler
+  switch (event) {
+  case TS_EVENT_NET_ACCEPT: {
+    // set up reader from client
+    TSVConn const downvc = static_cast<TSVConn>(edata);
+    data->m_dnstream.setupConnection(downvc);
+    data->m_dnstream.setupVioRead(contp, INT64_MAX);
+  } break;
+
+  case TS_EVENT_NET_ACCEPT_FAILED:
+  case TS_EVENT_VCONN_INACTIVITY_TIMEOUT:
+  case TS_EVENT_VCONN_ACTIVE_TIMEOUT:
+  case TS_EVENT_ERROR: {
+    abort(contp, data);
+  } break;
+
+  default: {
+    // data from client -- only the initial header
+    if (data->m_dnstream.m_read.isOpen() && edata == data->m_dnstream.m_read.m_vio) {
+      if (handle_client_req(contp, event, data)) {
+        // DEBUG_LOG("shutting down read from client pipe");
+        TSVConnShutdown(data->m_dnstream.m_vc, 1, 0);
+      }
+    }
+    // server wants more data from us, should never happen
+    // every time TSHttpConnect is called this resets
+    else if (data->m_upstream.m_write.isOpen() && edata == data->m_upstream.m_write.m_vio) {
+      // DEBUG_LOG("shutting down send to server pipe");
+      TSVConnShutdown(data->m_upstream.m_vc, 0, 1);
+    }
+    // server has data for us
+    else if (data->m_upstream.m_read.isOpen() && edata == data->m_upstream.m_read.m_vio) {
+      handle_server_resp(contp, event, data);
+    }
+    // client wants more data from us, only body content
+    else if (data->m_dnstream.m_write.isOpen() && edata == data->m_dnstream.m_write.m_vio) {
+      handle_client_resp(contp, event, data);
+    } else {
+      ERROR_LOG("Unhandled event: %d", event);
+    }
+  } break;
+  }
+
+  return TS_EVENT_CONTINUE;
+}
diff --git a/plugins/experimental/slice/intercept.h b/plugins/experimental/slice/intercept.h
new file mode 100644
index 0000000..3b59cef
--- /dev/null
+++ b/plugins/experimental/slice/intercept.h
@@ -0,0 +1,23 @@
+/** @file
+  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"
+
+int intercept_hook(TSCont contp, TSEvent event, void *edata);
diff --git a/plugins/experimental/slice/response.cc b/plugins/experimental/slice/response.cc
new file mode 100644
index 0000000..410b150
--- /dev/null
+++ b/plugins/experimental/slice/response.cc
@@ -0,0 +1,107 @@
+/** @file
+  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 "response.h"
+
+#include <cinttypes>
+#include <cstring>
+#include <mutex>
+
+#include "ts/ts.h"
+
+// canned body string for a 416, stolen from nginx
+std::string const &
+bodyString416()
+{
+  static std::string bodystr;
+  static std::mutex mutex;
+  std::lock_guard<std::mutex> const guard(mutex);
+
+  if (bodystr.empty()) {
+    bodystr.append("<html>\n");
+    bodystr.append("<head><title>416 Requested Range Not Satisfiable</title></head>\n");
+    bodystr.append("<body bgcolor=\"white\">\n");
+    bodystr.append("<center><h1>416 Requested Range Not Satisfiable</h1></center>");
+    bodystr.append("<hr><center>ATS/");
+    bodystr.append(TS_VERSION_STRING);
+    bodystr.append("</center>\n");
+    bodystr.append("</body>\n");
+    bodystr.append("</html>\n");
+  }
+
+  return bodystr;
+}
+
+// Form a 502 response, preliminary
+std::string const &
+string502()
+{
+  static std::string msg;
+  static std::mutex mutex;
+  std::lock_guard<std::mutex> const guard(mutex);
+
+  if (msg.empty()) {
+    std::string bodystr;
+    bodystr.append("<html>\n");
+    bodystr.append("<head><title>502 Bad Gateway</title></head>\n");
+    bodystr.append("<body bgcolor=\"white\">\n");
+    bodystr.append("<center><h1>502 Bad Gateway: Missing/Malformed "
+                   "Content-Range</h1></center>");
+    bodystr.append("<hr><center>ATS/");
+    bodystr.append(TS_VERSION_STRING);
+    bodystr.append("</center>\n");
+    bodystr.append("</body>\n");
+    bodystr.append("</html>\n");
+
+    char clenstr[1024];
+    int const clen = snprintf(clenstr, sizeof(clenstr), "%lu", bodystr.size());
+
+    msg.append("HTTP/1.1 502 Bad Gateway\r\n");
+    msg.append("Content-Length: ");
+    msg.append(clenstr, clen);
+    msg.append("\r\n");
+
+    msg.append("\r\n");
+    msg.append(bodystr);
+  }
+
+  return msg;
+}
+
+void
+form416HeaderAndBody(HttpHeader &header, int64_t const contentlen, std::string const &bodystr)
+{
+  header.removeKey(TS_MIME_FIELD_LAST_MODIFIED, TS_MIME_LEN_LAST_MODIFIED);
+  header.removeKey(TS_MIME_FIELD_ETAG, TS_MIME_LEN_ETAG);
+  header.removeKey(TS_MIME_FIELD_ACCEPT_RANGES, TS_MIME_LEN_ACCEPT_RANGES);
+
+  header.setStatus(TS_HTTP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE);
+  char const *const reason = TSHttpHdrReasonLookup(TS_HTTP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE);
+  header.setReason(reason, strlen(reason));
+
+  char bufstr[256];
+  int buflen = snprintf(bufstr, sizeof(bufstr), "%lu", bodystr.size());
+  header.setKeyVal(TS_MIME_FIELD_CONTENT_LENGTH, TS_MIME_LEN_CONTENT_LENGTH, bufstr, buflen);
+
+  static char const *const ctypestr = "text/html";
+  static int const ctypelen         = strlen(ctypestr);
+  header.setKeyVal(TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE, ctypestr, ctypelen);
+
+  buflen = snprintf(bufstr, 255, "*/%" PRId64, contentlen);
+  header.setKeyVal(TS_MIME_FIELD_CONTENT_RANGE, TS_MIME_LEN_CONTENT_RANGE, bufstr, buflen);
+}
diff --git a/plugins/experimental/slice/response.h b/plugins/experimental/slice/response.h
new file mode 100644
index 0000000..925db65
--- /dev/null
+++ b/plugins/experimental/slice/response.h
@@ -0,0 +1,28 @@
+/** @file
+  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 "HttpHeader.h"
+#include <string>
+
+std::string const &string502();
+
+std::string const &bodyString416();
+
+void form416HeaderAndBody(HttpHeader &header, int64_t const contentlen, std::string const &bodystr);
diff --git a/plugins/experimental/slice/server.cc b/plugins/experimental/slice/server.cc
new file mode 100644
index 0000000..5356806
--- /dev/null
+++ b/plugins/experimental/slice/server.cc
@@ -0,0 +1,467 @@
+/** @file
+  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 "server.h"
+
+#include "Config.h"
+#include "ContentRange.h"
+#include "response.h"
+#include "transfer.h"
+#include "util.h"
+
+#include "ts/experimental.h"
+
+#include <cinttypes>
+
+namespace
+{
+ContentRange
+contentRangeFrom(HttpHeader const &header)
+{
+  ContentRange bcr;
+
+  /* Pull content length off the response header
+    and manipulate it into a client response header
+   */
+  char rangestr[1024];
+  int rangelen = sizeof(rangestr);
+
+  // look for expected Content-Range field
+  bool const hasContentRange(header.valueForKey(TS_MIME_FIELD_CONTENT_RANGE, TS_MIME_LEN_CONTENT_RANGE, rangestr, &rangelen));
+
+  if (!hasContentRange) {
+    DEBUG_LOG("invalid response header, no Content-Range");
+  } else if (!bcr.fromStringClosed(rangestr)) {
+    DEBUG_LOG("invalid response header, malformed Content-Range, %s", rangestr);
+  }
+
+  return bcr;
+}
+
+bool
+handleFirstServerHeader(Data *const data, TSCont const contp)
+{
+  HttpHeader header(data->m_resp_hdrmgr.m_buffer, data->m_resp_hdrmgr.m_lochdr);
+
+  //  DEBUG_LOG("First header\n%s", header.toString().c_str());
+
+  data->m_dnstream.setupVioWrite(contp, INT64_MAX);
+
+  TSVIO const output_vio      = data->m_dnstream.m_write.m_vio;
+  TSIOBuffer const output_buf = data->m_dnstream.m_write.m_iobuf;
+
+  // only process a 206, everything else gets a (possibly incomplete)
+  // pass through
+  if (TS_HTTP_STATUS_PARTIAL_CONTENT != header.status()) {
+    DEBUG_LOG("Initial response other than 206: %d", header.status());
+
+    // Should run TSVIONSetBytes(output_io, hlen + bodybytes);
+    //    int const hlen = TSHttpHdrLengthGet(header.m_buffer, header.m_lochdr);
+    //    TSVIONBytesSet(output_vio, hlen);
+    TSHttpHdrPrint(header.m_buffer, header.m_lochdr, output_buf);
+    transfer_all_bytes(data);
+
+    return false;
+  }
+
+  ContentRange const blockcr = contentRangeFrom(header);
+  // 206 with bad content range?
+  if (!blockcr.isValid()) {
+    static std::string const &msg502 = string502();
+    TSVIONBytesSet(output_vio, msg502.size());
+    TSIOBufferWrite(output_buf, msg502.data(), msg502.size());
+    TSVIOReenable(output_vio);
+    return false;
+  }
+
+  // set the resource content length from block response
+  data->m_contentlen = blockcr.m_length;
+
+  // special case last N bytes
+  if (data->m_req_range.isEndBytes()) {
+    data->m_req_range.m_end += data->m_contentlen;
+    data->m_req_range.m_beg += data->m_contentlen;
+    data->m_req_range.m_beg = std::max(static_cast<int64_t>(0), data->m_req_range.m_beg);
+  } else {
+    // fix up request range end now that we have the content length
+    data->m_req_range.m_end = std::min(data->m_contentlen, data->m_req_range.m_end);
+  }
+
+  int64_t const bodybytes = data->m_req_range.size();
+
+  // range begins past end of data but inside last block, send 416
+  bool const send416 = (bodybytes <= 0 || TS_HTTP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE == data->m_statustype);
+  if (send416) {
+    std::string const &bodystr = bodyString416();
+    form416HeaderAndBody(header, data->m_contentlen, bodystr);
+
+    int const hlen     = TSHttpHdrLengthGet(header.m_buffer, header.m_lochdr);
+    int64_t const blen = bodystr.size();
+
+    TSVIONBytesSet(output_vio, int64_t(hlen) + blen);
+    TSHttpHdrPrint(header.m_buffer, header.m_lochdr, output_buf);
+    TSIOBufferWrite(output_buf, bodystr.data(), bodystr.size());
+    TSVIOReenable(output_vio);
+
+    data->m_upstream.m_read.close();
+
+    return false;
+  }
+
+  // save weak cache header identifiers (rfc7232 section 2)
+  data->m_etaglen = sizeof(data->m_etag) - 1;
+  header.valueForKey(TS_MIME_FIELD_ETAG, TS_MIME_LEN_ETAG, data->m_etag, &data->m_etaglen);
+  data->m_lastmodifiedlen = sizeof(data->m_lastmodified) - 1;
+  header.valueForKey(TS_MIME_FIELD_LAST_MODIFIED, TS_MIME_LEN_LAST_MODIFIED, data->m_lastmodified, &data->m_lastmodifiedlen);
+
+  // size of the first block payload
+  data->m_blockexpected = blockcr.rangeSize();
+
+  // Now we can set up the expected client response
+  if (TS_HTTP_STATUS_PARTIAL_CONTENT == data->m_statustype) {
+    ContentRange respcr;
+    respcr.m_beg    = data->m_req_range.m_beg;
+    respcr.m_end    = data->m_req_range.m_end;
+    respcr.m_length = data->m_contentlen;
+
+    char rangestr[1024];
+    int rangelen      = sizeof(rangestr);
+    bool const crstat = respcr.toStringClosed(rangestr, &rangelen);
+
+    // corner case, return 500 ??
+    if (!crstat) {
+      data->m_upstream.close();
+      data->m_dnstream.close();
+
+      ERROR_LOG("Bad/invalid response content range");
+      return false;
+    }
+
+    header.setKeyVal(TS_MIME_FIELD_CONTENT_RANGE, TS_MIME_LEN_CONTENT_RANGE, rangestr, rangelen);
+  } else if (TS_HTTP_STATUS_OK == data->m_statustype) {
+    header.setStatus(TS_HTTP_STATUS_OK);
+    static char const *const reason = TSHttpHdrReasonLookup(TS_HTTP_STATUS_OK);
+    header.setReason(reason, strlen(reason));
+    header.removeKey(TS_MIME_FIELD_CONTENT_RANGE, TS_MIME_LEN_CONTENT_RANGE);
+  }
+
+  char bufstr[1024];
+  int const buflen = snprintf(bufstr, sizeof(bufstr), "%" PRId64, bodybytes);
+  header.setKeyVal(TS_MIME_FIELD_CONTENT_LENGTH, TS_MIME_LEN_CONTENT_LENGTH, bufstr, buflen);
+
+  // add the response header length to the total bytes to send
+  int const hbytes = TSHttpHdrLengthGet(header.m_buffer, header.m_lochdr);
+
+  TSVIONBytesSet(output_vio, hbytes + bodybytes);
+  data->m_bytestosend = hbytes + bodybytes;
+  TSHttpHdrPrint(header.m_buffer, header.m_lochdr, output_buf);
+  data->m_bytessent = hbytes;
+  TSVIOReenable(output_vio);
+
+  return true;
+}
+
+void
+logSliceError(char const *const message, Data const *const data, HttpHeader const &header_resp)
+{
+  Config *const config = data->m_config;
+
+  bool const logToError = config->canLogError();
+
+  // always write block stitch errors while in debug mode
+  if (!logToError && !TSIsDebugTagSet(PLUGIN_NAME)) {
+    return;
+  }
+
+  HttpHeader const header_req(data->m_req_hdrmgr.m_buffer, data->m_req_hdrmgr.m_lochdr);
+
+  TSHRTime const timenowus = TShrtime();
+  int64_t const msecs      = timenowus / 1000000;
+  int64_t const secs       = msecs / 1000;
+  int64_t const ms         = msecs % 1000;
+
+  // Gather information on the request, must delete urlstr
+  int urllen         = 0;
+  char *const urlstr = header_req.urlString(&urllen);
+
+  char urlpstr[16384];
+  size_t urlplen = sizeof(urlpstr);
+  TSStringPercentEncode(urlstr, urllen, urlpstr, urlplen, &urlplen, nullptr);
+
+  if (nullptr != urlstr) {
+    TSfree(urlstr);
+  }
+
+  // uas
+  char uasstr[8192];
+  int uaslen = sizeof(uasstr);
+  header_req.valueForKey(TS_MIME_FIELD_USER_AGENT, TS_MIME_LEN_USER_AGENT, uasstr, &uaslen);
+
+  // raw range request
+  char rangestr[1024];
+  int rangelen = sizeof(rangestr);
+  header_req.valueForKey(SLICER_MIME_FIELD_INFO, strlen(SLICER_MIME_FIELD_INFO), rangestr, &rangelen);
+
+  // Normalized range request
+  ContentRange const crange(data->m_req_range.m_beg, data->m_req_range.m_end, data->m_contentlen);
+  char normstr[1024];
+  int normlen = sizeof(normstr);
+  crange.toStringClosed(normstr, &normlen);
+
+  // block range request
+  int64_t const blockbeg = data->m_blocknum * data->m_config->m_blockbytes;
+  int64_t const blockend = std::min(blockbeg + data->m_config->m_blockbytes, data->m_contentlen);
+
+  // Block response data
+  TSHttpStatus const statusgot = header_resp.status();
+
+  // content range
+  char crstr[1024];
+  int crlen = sizeof(crstr);
+  header_resp.valueForKey(TS_MIME_FIELD_CONTENT_RANGE, TS_MIME_LEN_CONTENT_RANGE, crstr, &crlen);
+
+  // etag
+  char etagstr[1024];
+  int etaglen = sizeof(etagstr);
+  header_resp.valueForKey(TS_MIME_FIELD_ETAG, TS_MIME_LEN_ETAG, etagstr, &etaglen);
+
+  // last modified
+  char lmstr[1024];
+  int lmlen = sizeof(lmstr);
+  header_resp.valueForKey(TS_MIME_FIELD_LAST_MODIFIED, TS_MIME_LEN_LAST_MODIFIED, lmstr, &lmlen);
+
+  // cc
+  char ccstr[2048];
+  int cclen = sizeof(ccstr);
+  header_resp.valueForKey(TS_MIME_FIELD_CACHE_CONTROL, TS_MIME_LEN_CACHE_CONTROL, ccstr, &cclen);
+
+  // via tag
+  char viastr[8192];
+  int vialen = sizeof(viastr);
+  header_resp.valueForKey(TS_MIME_FIELD_VIA, TS_MIME_LEN_VIA, viastr, &vialen);
+
+  char etagexpstr[1024];
+  size_t etagexplen = sizeof(etagexpstr);
+  TSStringPercentEncode(data->m_etag, data->m_etaglen, etagexpstr, etagexplen, &etagexplen, nullptr);
+
+  char etaggotstr[1024];
+  size_t etaggotlen = sizeof(etaggotstr);
+  TSStringPercentEncode(etagstr, etaglen, etaggotstr, etaggotlen, &etaggotlen, nullptr);
+
+  DEBUG_LOG("Logging Block Stitch error");
+
+  ERROR_LOG("%" PRId64 ".%" PRId64 " reason=\"%s\""
+            " uri=\"%.*s\""
+            " uas=\"%.*s\""
+            " req_range=\"%.*s\""
+            " norm_range=\"%.*s\""
+
+            " etag_exp=\"%.*s\""
+            " lm_exp=\"%.*s\""
+
+            " blk_range=\"%" PRId64 "-%" PRId64 "\""
+
+            " status_got=\"%d\""
+            " cr_got=\"%.*s\""
+            " etag_got=\"%.*s\""
+            " lm_got=\"%.*s\""
+            " cc=\"%.*s\""
+            " via=\"%.*s\"",
+            secs, ms, message, (int)urlplen, urlpstr, uaslen, uasstr, rangelen, rangestr, normlen, normstr, (int)etagexplen,
+            etagexpstr, data->m_lastmodifiedlen, data->m_lastmodified, blockbeg, blockend - 1, statusgot, crlen, crstr,
+            (int)etaggotlen, etaggotstr, lmlen, lmstr, cclen, ccstr, vialen, viastr);
+}
+
+bool
+handleNextServerHeader(Data *const data, TSCont const contp)
+{
+  // block response header
+  HttpHeader header(data->m_resp_hdrmgr.m_buffer, data->m_resp_hdrmgr.m_lochdr);
+  //  DEBUG_LOG("Next Header:\n%s", header.toString().c_str());
+
+  // only process a 206, everything else just aborts
+  if (TS_HTTP_STATUS_PARTIAL_CONTENT != header.status()) {
+    logSliceError("Non 206 internal block response", data, header);
+    return false;
+  }
+
+  // can't parse the content range header, abort -- might be too strict
+  ContentRange const blockcr = contentRangeFrom(header);
+  if (!blockcr.isValid() || blockcr.m_length != data->m_contentlen) {
+    logSliceError("Mismatch/Bad block Content-Range", data, header);
+    return false;
+  }
+
+  bool same = true;
+
+  // prefer the etag but use Last-Modified if we must.
+  char etag[8192];
+  int etaglen = sizeof(etag);
+  header.valueForKey(TS_MIME_FIELD_ETAG, TS_MIME_LEN_ETAG, etag, &etaglen);
+
+  if (0 < data->m_etaglen || 0 < etaglen) {
+    same = data->m_etaglen == etaglen && 0 == strncmp(etag, data->m_etag, etaglen);
+    if (!same) {
+      logSliceError("Mismatch block Etag", data, header);
+    }
+  } else {
+    char lastmodified[8192];
+    int lastmodifiedlen = sizeof(lastmodified);
+    header.valueForKey(TS_MIME_FIELD_LAST_MODIFIED, TS_MIME_LEN_LAST_MODIFIED, lastmodified, &lastmodifiedlen);
+    if (0 < data->m_lastmodifiedlen || 0 != lastmodifiedlen) {
+      same = data->m_lastmodifiedlen == lastmodifiedlen && 0 == strncmp(lastmodified, data->m_lastmodified, lastmodifiedlen);
+      if (!same) {
+        logSliceError("Mismatch block Last-Modified", data, header);
+      }
+    }
+  }
+
+  if (!same) {
+    data->m_upstream.close();
+    return false;
+  }
+
+  data->m_blockexpected = blockcr.rangeSize();
+
+  return true;
+}
+
+} // namespace
+
+// this is called every time the server has data for us
+void
+handle_server_resp(TSCont contp, TSEvent event, Data *const data)
+{
+  DEBUG_LOG("%p handle_server_resp: %s", data, TSHttpEventNameLookup(event));
+
+#if defined(COLLECT_STATS)
+  TSStatIntIncrement(stats::Server, 1);
+#endif
+
+  switch (event) {
+  case TS_EVENT_VCONN_READ_READY: {
+    // has block response header been parsed??
+    if (!data->m_server_block_header_parsed) {
+      int64_t consumed              = 0;
+      TSIOBufferReader const reader = data->m_upstream.m_read.m_reader;
+      TSVIO const input_vio         = data->m_upstream.m_read.m_vio;
+      TSParseResult const res       = data->m_resp_hdrmgr.populateFrom(data->m_http_parser, reader, TSHttpHdrParseResp, &consumed);
+
+      TSVIONDoneSet(input_vio, TSVIONDoneGet(input_vio) + consumed);
+
+      // the server response header didn't fit into the input buffer.
+      if (TS_PARSE_CONT == res) {
+        return;
+      }
+
+      // very first server response header
+      bool headerStat = false;
+
+      if (TS_PARSE_DONE == res) {
+        if (!data->m_server_first_header_parsed) {
+          headerStat                         = handleFirstServerHeader(data, contp);
+          data->m_server_first_header_parsed = true;
+        } else {
+          headerStat = handleNextServerHeader(data, contp);
+        }
+
+        data->m_server_block_header_parsed = true;
+      }
+
+      // kill the upstream and allow dnstream to clean up
+      if (!headerStat) {
+        data->m_upstream.abort();
+        data->m_blockstate = Data::BlockState::Fail;
+        if (data->m_dnstream.m_write.isOpen()) {
+          TSVIOReenable(data->m_dnstream.m_write.m_vio);
+        } else {
+          shutdown(contp, data);
+        }
+        return;
+      }
+
+      // how much to fast forward into this data block
+      data->m_blockskip = data->m_req_range.skipBytesForBlock(data->m_config->m_blockbytes, data->m_blocknum);
+    }
+
+    transfer_content_bytes(data);
+  } break;
+  case TS_EVENT_VCONN_READ_COMPLETE: {
+    // fprintf(stderr, "%p: TS_EVENT_VCONN_READ_COMPLETE\n", data);
+  } break;
+  case TS_EVENT_VCONN_EOS: {
+    data->m_blockstate = Data::BlockState::Pending;
+    data->m_upstream.close();
+
+    // check for block truncation
+    if (data->m_blockconsumed < data->m_blockexpected) {
+      DEBUG_LOG("%p handle_server_resp truncation: %" PRId64 "\n", data, data->m_blockexpected - data->m_blockconsumed);
+      data->m_blockstate = Data::BlockState::Fail;
+      //      shutdown(contp, data);
+      return;
+    }
+
+    // prepare for the next request block
+    ++data->m_blocknum;
+
+    // when we get a "bytes=-<end>" last N bytes request the plugin
+    // issues a speculative request for the first block
+    // in that case fast forward to the real first in range block
+    // Btw this isn't implemented yet, to be handled
+    int64_t const firstblock = data->m_req_range.firstBlockFor(data->m_config->m_blockbytes);
+    if (data->m_blocknum < firstblock) {
+      data->m_blocknum = firstblock;
+    }
+
+    // continue processing blocks?
+    if (data->m_req_range.blockIsInside(data->m_config->m_blockbytes, data->m_blocknum)) {
+      // Don't immediately request the next slice if the client
+      // isn't keeping up
+
+      bool start_next_block = true;
+
+      // throttle condition
+      if (data->m_config->m_throttle && data->m_dnstream.m_read.isOpen()) {
+        TSVIO const output_vio    = data->m_dnstream.m_write.m_vio;
+        int64_t const output_done = TSVIONDoneGet(output_vio);
+        int64_t const output_sent = data->m_bytessent;
+        int64_t const threshout   = data->m_config->m_blockbytes;
+
+        if (threshout < (output_done - output_sent)) {
+          start_next_block = false;
+          DEBUG_LOG("%p handle_server_resp: throttling %" PRId64, data, (output_done - output_sent));
+        }
+      }
+
+      if (start_next_block) {
+        request_block(contp, data);
+      }
+
+    } else {
+      data->m_upstream.close();
+      data->m_blockstate = Data::BlockState::Done;
+      if (!data->m_dnstream.m_read.isOpen()) {
+        shutdown(contp, data);
+      }
+    }
+  } break;
+  default: {
+    DEBUG_LOG("%p handle_server_resp uhandled event: %s", data, TSHttpEventNameLookup(event));
+  } break;
+  }
+}
diff --git a/plugins/experimental/slice/server.h b/plugins/experimental/slice/server.h
new file mode 100644
index 0000000..c0d77e4
--- /dev/null
+++ b/plugins/experimental/slice/server.h
@@ -0,0 +1,37 @@
+/** @file
+  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 "Data.h"
+
+#include "ts/ts.h"
+
+/** Functions to handle the connection to the server.
+ * In particular slice block header responses are handled here.
+ * Data transfers are handled by the client code which pulls
+ * the data from the server side.
+ *
+ * Special case is when the client connection has been closed
+ * because of client data request being fulfilled or
+ * when the client aborts.  The current slice block will
+ * continue reading to ensure the whole block is transferred
+ * to cache.
+ */
+
+void handle_server_resp(TSCont contp, TSEvent event, Data *const data);
diff --git a/plugins/experimental/slice/slice.cc b/plugins/experimental/slice/slice.cc
new file mode 100644
index 0000000..0109f20
--- /dev/null
+++ b/plugins/experimental/slice/slice.cc
@@ -0,0 +1,308 @@
+/** @file
+  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 "slice.h"
+
+#include "Config.h"
+#include "Data.h"
+#include "HttpHeader.h"
+#include "intercept.h"
+
+#include "ts/remap.h"
+#include "ts/ts.h"
+
+#include <netinet/in.h>
+#include <cassert>
+#include <mutex>
+
+#if defined(COLLECT_STATS)
+namespace stats
+{
+int DataCreate  = -1;
+int DataDestroy = -1;
+int Reader      = -1;
+int Server      = -1;
+int Client      = -1;
+} // namespace stats
+#endif // COLLECT_STATS
+
+namespace
+{
+Config globalConfig;
+
+bool
+read_request(TSHttpTxn txnp, Config *const config)
+{
+  DEBUG_LOG("slice read_request");
+  TxnHdrMgr hdrmgr;
+  hdrmgr.populateFrom(txnp, TSHttpTxnClientReqGet);
+  HttpHeader const header(hdrmgr.m_buffer, hdrmgr.m_lochdr);
+
+  if (TS_HTTP_METHOD_GET == header.method()) {
+    static int const SLICER_MIME_LEN_INFO = strlen(SLICER_MIME_FIELD_INFO);
+    if (!header.hasKey(SLICER_MIME_FIELD_INFO, SLICER_MIME_LEN_INFO)) {
+      // check if any previous plugin has monkeyed with the transaction status
+      TSHttpStatus const txnstat = TSHttpTxnStatusGet(txnp);
+      if (0 != (int)txnstat) {
+        DEBUG_LOG("slice: txn status change detected (%d), skipping plugin\n", (int)txnstat);
+        return false;
+      }
+
+      // turn off any and all transaction caching (shouldn't matter)
+      TSHttpTxnServerRespNoStoreSet(txnp, 1);
+      TSHttpTxnRespCacheableSet(txnp, 0);
+      TSHttpTxnReqCacheableSet(txnp, 0);
+
+      DEBUG_LOG("slice accepting and slicing");
+      // connection back into ATS
+      sockaddr const *const ip = TSHttpTxnClientAddrGet(txnp);
+      if (nullptr == ip) {
+        return false;
+      }
+
+      TSAssert(nullptr != config);
+      Data *const data = new Data(config);
+
+      // set up feedback connect
+      if (AF_INET == ip->sa_family) {
+        memcpy(&data->m_client_ip, ip, sizeof(sockaddr_in));
+      } else if (AF_INET6 == ip->sa_family) {
+        memcpy(&data->m_client_ip, ip, sizeof(sockaddr_in6));
+      } else {
+        delete data;
+        return false;
+      }
+
+      // need to reset the HOST field for global plugin
+      data->m_hostlen = sizeof(data->m_hostname) - 1;
+      if (!header.valueForKey(TS_MIME_FIELD_HOST, TS_MIME_LEN_HOST, data->m_hostname, &data->m_hostlen)) {
+        DEBUG_LOG("Unable to get hostname from header");
+        delete data;
+        return false;
+      }
+
+      // is the plugin configured to use a remap host?
+      std::string const &newhost = config->m_remaphost;
+      if (newhost.empty()) {
+        TSMBuffer urlbuf;
+        TSMLoc urlloc;
+        TSReturnCode rcode = TSHttpTxnPristineUrlGet(txnp, &urlbuf, &urlloc);
+
+        if (TS_SUCCESS == rcode) {
+          TSMBuffer const newbuf = TSMBufferCreate();
+          TSMLoc newloc          = nullptr;
+          rcode                  = TSUrlClone(newbuf, urlbuf, urlloc, &newloc);
+          TSHandleMLocRelease(urlbuf, TS_NULL_MLOC, urlloc);
+
+          if (TS_SUCCESS != rcode) {
+            ERROR_LOG("Error cloning pristine url");
+            TSMBufferDestroy(newbuf);
+            delete data;
+            return false;
+          }
+
+          data->m_urlbuf = newbuf;
+          data->m_urlloc = newloc;
+        }
+      } else { // grab the effective url, swap out the host and zero the port
+        int len            = 0;
+        char *const effstr = TSHttpTxnEffectiveUrlStringGet(txnp, &len);
+
+        if (nullptr != effstr) {
+          TSMBuffer const newbuf = TSMBufferCreate();
+          TSMLoc newloc          = nullptr;
+          bool okay              = false;
+
+          if (TS_SUCCESS == TSUrlCreate(newbuf, &newloc)) {
+            char const *start = effstr;
+            if (TS_PARSE_DONE == TSUrlParse(newbuf, newloc, &start, start + len)) {
+              if (TS_SUCCESS == TSUrlHostSet(newbuf, newloc, newhost.c_str(), newhost.size()) &&
+                  TS_SUCCESS == TSUrlPortSet(newbuf, newloc, 0)) {
+                okay = true;
+              }
+            }
+          }
+
+          TSfree(effstr);
+
+          if (!okay) {
+            ERROR_LOG("Error cloning effective url");
+            if (nullptr != newloc) {
+              TSHandleMLocRelease(newbuf, nullptr, newloc);
+            }
+            TSMBufferDestroy(newbuf);
+            delete data;
+            return false;
+          }
+
+          data->m_urlbuf = newbuf;
+          data->m_urlloc = newloc;
+        }
+      }
+
+      if (TSIsDebugTagSet(PLUGIN_NAME)) {
+        int len            = 0;
+        char *const urlstr = TSUrlStringGet(data->m_urlbuf, data->m_urlloc, &len);
+        DEBUG_LOG("slice url: %.*s", len, urlstr);
+        TSfree(urlstr);
+      }
+
+      // we'll intercept this GET and do it ourselves
+      TSMutex const mutex = TSContMutexGet(reinterpret_cast<TSCont>(txnp));
+      // TSMutex const mutex = TSMutexCreate();
+      TSCont const icontp(TSContCreate(intercept_hook, mutex));
+      TSContDataSet(icontp, (void *)data);
+      TSHttpTxnIntercept(icontp, txnp);
+      return true;
+    } else {
+      DEBUG_LOG("slice passing GET request through to next plugin");
+    }
+  }
+
+  return false;
+}
+
+int
+global_read_request_hook(TSCont // contp
+                         ,
+                         TSEvent // event
+                         ,
+                         void *edata)
+{
+  TSHttpTxn const txnp = static_cast<TSHttpTxn>(edata);
+  read_request(txnp, &globalConfig);
+  TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+  return 0;
+}
+
+} // namespace
+
+///// remap plugin engine
+
+SLICE_EXPORT
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri)
+{
+  Config *const config = static_cast<Config *>(ih);
+
+  if (read_request(txnp, config)) {
+    return TSREMAP_DID_REMAP_STOP;
+  } else {
+    return TSREMAP_NO_REMAP;
+  }
+}
+
+///// remap plugin setup and teardown
+SLICE_EXPORT
+void
+TSRemapOSResponse(void *ih, TSHttpTxn rh, int os_response_type)
+{
+}
+
+SLICE_EXPORT
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf */, int /* errbuf_size */)
+{
+  Config *const config = new Config;
+  if (2 < argc) {
+    config->fromArgs(argc - 2, argv + 2);
+  }
+  *ih = static_cast<void *>(config);
+  return TS_SUCCESS;
+}
+
+SLICE_EXPORT
+void
+TSRemapDeleteInstance(void *ih)
+{
+  if (nullptr != ih) {
+    Config *const config = static_cast<Config *>(ih);
+    delete config;
+  }
+}
+
+SLICE_EXPORT
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbug, int errbuf_size)
+{
+  DEBUG_LOG("slice remap initializing.");
+
+#if defined(COLLECT_STATS)
+  static bool init = false;
+  static std::mutex mutex;
+
+  std::lock_guard<std::mutex> lock(mutex);
+
+  if (!init) {
+    init = true;
+
+    std::string const namedatacreate = std::string(PLUGIN_NAME) + ".DataCreate";
+    stats::DataCreate = TSStatCreate(namedatacreate.c_str(), TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+
+    assert(0 <= stats::DataCreate);
+
+    std::string const namedatadestroy = std::string(PLUGIN_NAME) + ".DataDestroy";
+    stats::DataDestroy = TSStatCreate(namedatadestroy.c_str(), TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+
+    assert(0 <= stats::DataDestroy);
+
+    std::string const namereader = std::string(PLUGIN_NAME) + ".Reader";
+    stats::Reader = TSStatCreate(namereader.c_str(), TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+
+    assert(0 <= stats::Reader);
+
+    std::string const nameserver = std::string(PLUGIN_NAME) + ".Server";
+    stats::Server = TSStatCreate(nameserver.c_str(), TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+
+    assert(0 <= stats::Server);
+
+    std::string const nameclient = std::string(PLUGIN_NAME) + ".Client";
+    stats::Client = TSStatCreate(nameclient.c_str(), TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+
+    assert(0 <= stats::Client);
+  }
+#endif // COLLECT_STATS
+
+  return TS_SUCCESS;
+}
+
+///// global plugin
+SLICE_EXPORT
+void
+TSPluginInit(int argc, char const *argv[])
+{
+  TSPluginRegistrationInfo info;
+  info.plugin_name   = (char *)PLUGIN_NAME;
+  info.vendor_name   = (char *)"Apache Software Foundation";
+  info.support_email = (char *)"dev@trafficserver.apache.org";
+
+  if (TS_SUCCESS != TSPluginRegister(&info)) {
+    ERROR_LOG("Plugin registration failed.\n");
+    ERROR_LOG("Unable to initialize plugin (disabled).");
+    return;
+  }
+
+  if (1 < argc) {
+    globalConfig.fromArgs(argc - 1, argv + 1);
+  }
+
+  TSCont const contp(TSContCreate(global_read_request_hook, nullptr));
+
+  // Called immediately after the request header is read from the client
+  TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, contp);
+}
diff --git a/plugins/experimental/slice/slice.h b/plugins/experimental/slice/slice.h
new file mode 100644
index 0000000..d4e040b
--- /dev/null
+++ b/plugins/experimental/slice/slice.h
@@ -0,0 +1,65 @@
+/** @file
+  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 <cstring>
+
+#ifndef SLICE_EXPORT
+#define SLICE_EXPORT extern "C" tsapi
+#endif
+
+#ifndef PLUGIN_NAME
+#define PLUGIN_NAME "slice"
+#endif
+
+#if !defined(UNITTEST)
+
+#define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
+#define DEBUG_LOG(fmt, ...)                                                      \
+  TSDebug(PLUGIN_NAME, "[%s:%04d] %s(): " fmt, __FILENAME__, __LINE__, __func__, \
+          ##__VA_ARGS__) /*                                                      \
+                                 ; fprintf(stderr, "[%s:%04d]: " fmt "\n"        \
+                                         , __FILENAME__                          \
+                                         , __LINE__                              \
+                                         , ##__VA_ARGS__)                        \
+                         */
+
+#define ERROR_LOG(fmt, ...)                                                         \
+  TSError("[%s:%04d] %s(): " fmt, __FILENAME__, __LINE__, __func__, ##__VA_ARGS__); \
+  TSDebug(PLUGIN_NAME, "[%s:%04d] %s(): " fmt, __FILENAME__, __LINE__, __func__, ##__VA_ARGS__)
+
+#else
+
+#define DEBUG_LOG(fmt, ...)
+#define ERROR_LOG(fmt, ...)
+
+#endif
+
+#if defined(COLLECT_STATS)
+namespace stats
+{
+extern int DataCreate;
+extern int DataDestroy;
+extern int Reader;
+extern int Server;
+extern int Client;
+} // namespace stats
+#endif // COLLECT_STATS
diff --git a/plugins/experimental/slice/slice_test.cc b/plugins/experimental/slice/slice_test.cc
new file mode 100644
index 0000000..4fcbc5b
--- /dev/null
+++ b/plugins/experimental/slice/slice_test.cc
@@ -0,0 +1,195 @@
+/** @file
+  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.
+ */
+
+/*
+ * These are misc unit tests for slicer
+ */
+
+#include "ContentRange.h"
+#include "Range.h"
+
+#include <cassert>
+#include <iostream>
+#include <limits>
+#include <sstream>
+#include <vector>
+
+std::string
+testContentRange()
+{
+  std::ostringstream oss;
+
+  ContentRange null;
+  if (null.isValid()) {
+    oss << "fail: null isValid test" << std::endl;
+  }
+
+  ContentRange const exprange(1023, 1048576, 307232768);
+
+  if (!exprange.isValid()) {
+    oss << "Fail: exprange valid" << std::endl;
+    oss << exprange.m_beg << ' ' << exprange.m_end << ' ' << exprange.m_length << std::endl;
+  }
+
+  std::string const expstr("bytes 1023-1048575/307232768");
+
+  char gotbuf[1024];
+  int gotlen = sizeof(gotbuf);
+
+  bool const strstat(exprange.toStringClosed(gotbuf, &gotlen));
+
+  if (!strstat) {
+    oss << "failure status toStringClosed" << std::endl;
+  } else if ((int)expstr.size() != gotlen) {
+    oss << "Fail: expected toStringClosed length" << std::endl;
+    oss << "got: " << gotlen << " exp: " << expstr.size() << std::endl;
+    oss << "Got: " << gotbuf << std::endl;
+    oss << "Exp: " << expstr << std::endl;
+  } else if (expstr != gotbuf) {
+    oss << "Fail: expected toStringClosed value" << std::endl;
+    oss << "Got: " << gotbuf << std::endl;
+    oss << "Exp: " << expstr << std::endl;
+  }
+
+  ContentRange gotrange;
+  bool const gotstat(gotrange.fromStringClosed(expstr.c_str()));
+  if (!gotstat) {
+    oss << "fail: gotstat from string" << std::endl;
+  } else if (gotrange.m_beg != exprange.m_beg || gotrange.m_end != exprange.m_end || gotrange.m_length != exprange.m_length) {
+    oss << "fail: value compare gotrange and exprange" << std::endl;
+  }
+
+  std::string const teststr("bytes 0-1048575/30723276");
+  if (!gotrange.fromStringClosed(teststr.c_str())) {
+    oss << "fail: parse teststr" << std::endl;
+  }
+
+  return oss.str();
+}
+
+std::string
+testParseRange()
+{
+  std::ostringstream oss;
+
+  std::vector<std::string> const teststrings = {
+    "bytes=0-1023",
+    "bytes=1-1024",
+    "bytes=11-11",
+    "bytes=1-" // 2nd byte to end
+    ,
+    "Range: bytes=-13" // final 13 bytes
+    ,
+    "bytes=3-17" // ,23-29" // open
+    ,
+    "bytes=3 -17 " //,18-29" // adjacent
+    ,
+    "bytes=3- 17" //, 11-29" // overlapping
+    ,
+    "bytes=3 - 11" //,13-17 , 23-29" // unsorted triplet
+    ,
+    "bytes=3-11 " //,13-17, 23-29" // unsorted triplet
+    ,
+    "bytes=0-0" //,-1" // first and last bytes
+    ,
+    "bytes=-20" // last 20 bytes of file
+
+    ,
+    "bytes=-60-50" // invalid fully negative
+    ,
+    "bytes=17-13" // degenerate
+    ,
+    "bytes 0-1023/146515" // this should be rejected (Content-range)
+  };                      // invalid
+
+  std::vector<Range> const exps = {Range{0, 1023 + 1}, Range{1, 1024 + 1}, Range{11, 11 + 1}, Range{1, Range::maxval},
+                                   Range{-1, -1},      Range{3, 17 + 1},   Range{3, 17 + 1},  Range{3, 17 + 1},
+                                   Range{3, 11 + 1},   Range{3, 11 + 1},   Range{0, 1},       Range{-20, 0},
+                                   Range{-1, -1},      Range{-1, -1},      Range{-1, -1}};
+
+  std::vector<bool> const expsres = {true, true, true, true, false, true, true, true, true, true, true, true, false, false, false};
+
+  assert(exps.size() == teststrings.size());
+
+  std::vector<Range> gots;
+  gots.reserve(exps.size());
+  std::vector<bool> gotsres;
+
+  for (std::string const &str : teststrings) {
+    Range rng;
+    gotsres.push_back(rng.fromStringClosed(str.c_str()));
+    gots.push_back(rng);
+  }
+
+  assert(gots.size() == exps.size());
+
+  for (size_t index(0); index < gots.size(); ++index) {
+    if (exps[index] != gots[index] || expsres[index] != gotsres[index]) {
+      oss << "Error parsing index: " << index << std::endl;
+      oss << "test: '" << teststrings[index] << "'" << std::endl;
+      oss << "exp: " << exps[index].m_beg << ' ' << exps[index].m_end << std::endl;
+      oss << "expsres: " << (int)expsres[index] << std::endl;
+      oss << "got: " << gots[index].m_beg << ' ' << gots[index].m_end << std::endl;
+      oss << "gotsres: " << (int)gotsres[index] << std::endl;
+    }
+  }
+
+  return oss.str();
+}
+
+struct Tests {
+  typedef std::string (*TestFunc)();
+  std::vector<std::pair<TestFunc, char const *>> funcs;
+
+  void
+  add(TestFunc const &func, char const *const fname)
+  {
+    funcs.push_back(std::make_pair(func, fname));
+  }
+
+  int
+  run() const
+  {
+    int numfailed(0);
+    for (std::pair<TestFunc, char const *> const &namefunc : funcs) {
+      TestFunc const &func   = namefunc.first;
+      char const *const name = namefunc.second;
+
+      std::cerr << name << " : ";
+
+      std::string const fres(func());
+      if (fres.empty()) {
+        std::cerr << "pass" << std::endl;
+      } else {
+        std::cerr << "FAIL" << std::endl;
+        std::cerr << fres << std::endl;
+        ++numfailed;
+      }
+    }
+    return numfailed;
+  }
+};
+
+int
+main()
+{
+  Tests tests;
+  tests.add(testContentRange, "testContentRange");
+  tests.add(testParseRange, "testParseRange");
+  return tests.run();
+}
diff --git a/plugins/experimental/slice/transfer.cc b/plugins/experimental/slice/transfer.cc
new file mode 100644
index 0000000..f83b088
--- /dev/null
+++ b/plugins/experimental/slice/transfer.cc
@@ -0,0 +1,151 @@
+/** @file
+  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 "transfer.h"
+
+int64_t
+transfer_content_bytes(Data *const data)
+{
+  // nothing to transfer if there's no source.
+  if (nullptr == data->m_upstream.m_read.m_reader) {
+    return 0;
+  }
+
+  TSIOBufferReader const reader = data->m_upstream.m_read.m_reader;
+  TSIOBuffer const output_buf   = data->m_dnstream.m_write.m_iobuf;
+  TSVIO const output_vio        = data->m_dnstream.m_write.m_vio;
+
+  int64_t consumed = 0; // input vio bytes visited
+  int64_t copied   = 0; // output bytes transferred
+
+  bool const canWrite = data->m_dnstream.m_write.isOpen();
+  bool done           = false;
+
+  TSIOBufferBlock block = TSIOBufferReaderStart(reader);
+
+  while (!done && nullptr != block) {
+    int64_t bavail = TSIOBufferBlockReadAvail(block, reader);
+
+    if (0 == bavail) {
+      block = TSIOBufferBlockNext(block);
+    } else {
+      int64_t toconsume = 0;
+
+      if (canWrite) {
+        int64_t const toskip = std::min(data->m_blockskip, bavail);
+        if (0 < toskip) { // before bytes
+          toconsume = toskip;
+          data->m_blockskip -= toskip;
+        } else {
+          int64_t const bytesleft = data->m_bytestosend - data->m_bytessent;
+          if (0 < bytesleft) { // transfer bytes
+            int64_t const tocopy = std::min(bavail, bytesleft);
+            int64_t const nbytes = TSIOBufferCopy(output_buf, reader, tocopy, 0);
+
+            done = (nbytes < tocopy); // output buffer stuffed
+
+            copied += nbytes;
+            data->m_bytessent += nbytes;
+
+            toconsume = nbytes;
+          } else { // after bytes
+            toconsume = bavail;
+          }
+        }
+      } else { // drain
+        toconsume = bavail;
+      }
+
+      if (0 < toconsume) {
+        if (bavail == toconsume) {
+          block = TSIOBufferBlockNext(block);
+        }
+        TSIOBufferReaderConsume(reader, toconsume);
+        consumed += toconsume;
+      }
+    }
+  }
+
+  // tell output more data is available
+  if (0 < copied) {
+    TSVIOReenable(output_vio);
+  }
+
+  if (0 < consumed) {
+    data->m_blockconsumed += consumed;
+
+    TSVIO const input_vio = data->m_upstream.m_read.m_vio;
+    if (nullptr != input_vio) {
+      TSVIONDoneSet(input_vio, TSVIONDoneGet(input_vio) + consumed);
+    }
+  }
+
+  return consumed;
+}
+
+// transfer all bytes from the server (error condition)
+int64_t
+transfer_all_bytes(Data *const data)
+{
+  // nothing to transfer if there's no source.
+  if (nullptr == data->m_upstream.m_read.m_reader || !data->m_dnstream.m_write.isOpen()) {
+    return 0;
+  }
+
+  int64_t consumed = 0; // input vio bytes visited
+
+  TSIOBufferReader const reader = data->m_upstream.m_read.m_reader;
+  TSIOBuffer const output_buf   = data->m_dnstream.m_write.m_iobuf;
+
+  bool done = false;
+
+  TSIOBufferBlock block = TSIOBufferReaderStart(reader);
+
+  while (!done && nullptr != block) {
+    int64_t bavail = TSIOBufferBlockReadAvail(block, reader);
+
+    if (0 == bavail) {
+      block = TSIOBufferBlockNext(block);
+    } else {
+      int64_t const nbytes = TSIOBufferCopy(output_buf, reader, bavail, 0);
+      done                 = nbytes < bavail; // output buffer is full
+
+      if (0 < nbytes) {
+        if (bavail == nbytes) {
+          block = TSIOBufferBlockNext(block);
+        }
+        TSIOBufferReaderConsume(reader, nbytes);
+        consumed += nbytes;
+      }
+    }
+  }
+
+  if (0 < consumed) {
+    TSVIO const output_vio = data->m_dnstream.m_write.m_vio;
+    if (nullptr != output_vio) {
+      TSVIOReenable(output_vio);
+    }
+
+    TSVIO const input_vio = data->m_upstream.m_read.m_vio;
+    if (nullptr != input_vio) {
+      TSVIONDoneSet(input_vio, TSVIONDoneGet(input_vio) + consumed);
+    }
+  }
+
+  return consumed;
+}
diff --git a/plugins/experimental/slice/transfer.h b/plugins/experimental/slice/transfer.h
new file mode 100644
index 0000000..a63bb24
--- /dev/null
+++ b/plugins/experimental/slice/transfer.h
@@ -0,0 +1,37 @@
+/** @file
+  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 "Data.h"
+
+/** Functions to deal with the connection to the client.
+ * Body content transfers are handled by the client.
+ * New block requests are also initiated by the client.
+ */
+
+/* transfer bytes from the server to the client
+ * Returns amount of bytes consumed from the reader (<= bytes written to client)
+ */
+int64_t transfer_content_bytes(Data *const data); // , char const * const fstr);
+
+// transfer all bytes from the server (error condition)
+int64_t transfer_all_bytes(Data *const data);
+
+// Signal the input about write state
+void signal_input(TSVIO const input_vio, int64_t const consumed);
diff --git a/plugins/experimental/slice/unit-tests/slice_test.cc b/plugins/experimental/slice/unit-tests/slice_test.cc
new file mode 100644
index 0000000..96ae726
--- /dev/null
+++ b/plugins/experimental/slice/unit-tests/slice_test.cc
@@ -0,0 +1,181 @@
+/** @file
+  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.
+ */
+
+/*
+ * These are misc unit tests for slicer
+ */
+
+#include "ContentRange.h"
+#include "Range.h"
+
+#include <cassert>
+#include <iostream>
+#include <limits>
+#include <sstream>
+#include <vector>
+
+std::string
+testContentRange()
+{
+  std::ostringstream oss;
+
+  ContentRange null;
+  if (null.isValid()) {
+    oss << "fail: null isValid test" << std::endl;
+  }
+
+  ContentRange const exprange(1023, 1048576, 307232768);
+
+  if (!exprange.isValid()) {
+    oss << "Fail: exprange valid" << std::endl;
+    oss << exprange.m_beg << ' ' << exprange.m_end << ' ' << exprange.m_length << std::endl;
+  }
+
+  std::string const expstr("bytes 1023-1048575/307232768");
+
+  char gotbuf[1024];
+  int gotlen = sizeof(gotbuf);
+
+  bool const strstat(exprange.toStringClosed(gotbuf, &gotlen));
+
+  if (!strstat) {
+    oss << "failure status toStringClosed" << std::endl;
+  } else if ((int)expstr.size() != gotlen) {
+    oss << "Fail: expected toStringClosed length" << std::endl;
+    oss << "got: " << gotlen << " exp: " << expstr.size() << std::endl;
+    oss << "Got: " << gotbuf << std::endl;
+    oss << "Exp: " << expstr << std::endl;
+  } else if (expstr != gotbuf) {
+    oss << "Fail: expected toStringClosed value" << std::endl;
+    oss << "Got: " << gotbuf << std::endl;
+    oss << "Exp: " << expstr << std::endl;
+  }
+
+  ContentRange gotrange;
+  bool const gotstat(gotrange.fromStringClosed(expstr.c_str()));
+  if (!gotstat) {
+    oss << "fail: gotstat from string" << std::endl;
+  } else if (gotrange.m_beg != exprange.m_beg || gotrange.m_end != exprange.m_end || gotrange.m_length != exprange.m_length) {
+    oss << "fail: value compare gotrange and exprange" << std::endl;
+  }
+
+  std::string const teststr("bytes 0-1048575/30723276");
+  if (!gotrange.fromStringClosed(teststr.c_str())) {
+    oss << "fail: parse teststr" << std::endl;
+  }
+
+  return oss.str();
+}
+
+std::string
+testParseRange()
+{
+  std::ostringstream oss;
+
+  std::vector<std::string> const teststrings = {
+    "bytes=0-1023",       "bytes=1-1024", "bytes=11-11",
+    "bytes=1-",           // 2nd byte to end
+    "Range: bytes=-13",   // final 13 bytes
+    "bytes=3-17",         // ,23-29" // open
+    "bytes=3 -17 ",       //,18-29" // adjacent
+    "bytes=3- 17",        //, 11-29" // overlapping
+    "bytes=3 - 11",       //,13-17 , 23-29" // unsorted triplet
+    "bytes=3-11 ",        //,13-17, 23-29" // unsorted triplet
+    "bytes=0-0",          //,-1" // first and last bytes
+    "bytes=-20",          // last 20 bytes of file
+    "bytes=-60-50",       // invalid fully negative
+    "bytes=17-13",        // degenerate
+    "bytes 0-1023/146515" // this should be rejected (Content-range)
+  };                      // invalid
+
+  std::vector<Range> const exps = {Range{0, 1023 + 1}, Range{1, 1024 + 1}, Range{11, 11 + 1}, Range{1, Range::maxval},
+                                   Range{-1, -1},      Range{3, 17 + 1},   Range{3, 17 + 1},  Range{3, 17 + 1},
+                                   Range{3, 11 + 1},   Range{3, 11 + 1},   Range{0, 1},       Range{-20, 0},
+                                   Range{-1, -1},      Range{-1, -1},      Range{-1, -1}};
+
+  std::vector<bool> const expsres = {true, true, true, true, false, true, true, true, true, true, true, true, false, false, false};
+
+  assert(exps.size() == teststrings.size());
+
+  std::vector<Range> gots;
+  gots.reserve(exps.size());
+  std::vector<bool> gotsres;
+
+  for (std::string const &str : teststrings) {
+    Range rng;
+    gotsres.push_back(rng.fromStringClosed(str.c_str()));
+    gots.push_back(rng);
+  }
+
+  assert(gots.size() == exps.size());
+
+  for (size_t index(0); index < gots.size(); ++index) {
+    if (exps[index] != gots[index] || expsres[index] != gotsres[index]) {
+      oss << "Eror parsing index: " << index << std::endl;
+      oss << "test: '" << teststrings[index] << "'" << std::endl;
+      oss << "exp: " << exps[index].m_beg << ' ' << exps[index].m_end << std::endl;
+      oss << "expsres: " << (int)expsres[index] << std::endl;
+      oss << "got: " << gots[index].m_beg << ' ' << gots[index].m_end << std::endl;
+      oss << "gotsres: " << (int)gotsres[index] << std::endl;
+    }
+  }
+
+  return oss.str();
+}
+
+struct Tests {
+  typedef std::string (*TestFunc)();
+  std::vector<std::pair<TestFunc, char const *>> funcs;
+
+  void
+  add(TestFunc const &func, char const *const fname)
+  {
+    funcs.push_back(std::make_pair(func, fname));
+  }
+
+  int
+  run() const
+  {
+    int numfailed(0);
+    for (std::pair<TestFunc, char const *> const &namefunc : funcs) {
+      TestFunc const &func   = namefunc.first;
+      char const *const name = namefunc.second;
+
+      std::cerr << name << " : ";
+
+      std::string const fres(func());
+      if (fres.empty()) {
+        std::cerr << "pass" << std::endl;
+      } else {
+        std::cerr << "FAIL" << std::endl;
+        std::cerr << fres << std::endl;
+        ++numfailed;
+      }
+    }
+    return numfailed;
+  }
+};
+
+int
+main()
+{
+  Tests tests;
+  tests.add(testContentRange, "testContentRange");
+  tests.add(testParseRange, "testParseRange");
+  return tests.run();
+}
diff --git a/plugins/experimental/slice/unit-tests/test_config.cc b/plugins/experimental/slice/unit-tests/test_config.cc
new file mode 100644
index 0000000..b85f381
--- /dev/null
+++ b/plugins/experimental/slice/unit-tests/test_config.cc
@@ -0,0 +1,86 @@
+/** @file
+  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.
+ */
+
+/**
+ * @file test_content_range.cc
+ * @brief Unit test for slice ContentRange
+ */
+
+#define CATCH_CONFIG_MAIN /* include main function */
+#include "../Config.h"
+#include "catch.hpp" /* catch unit-test framework */
+
+#include <array>
+
+TEST_CASE("config default", "[AWS][slice][utility]")
+{
+  Config const config;
+  int64_t const defval = Config::blockbytesdefault;
+  CHECK(defval == config.m_blockbytes);
+}
+
+TEST_CASE("config bytesfrom valid parsing", "[AWS][slice][utility]")
+{
+  static std::array<std::string, 6> const teststrings = {{
+    "1000",
+    "1m",
+    "5g",
+    "2k",
+    "3kb",
+    "1z",
+  }};
+
+  constexpr std::array<int64_t, 6> const expvals = {{
+    1000,
+    1024 * 1024,
+    int64_t(1024) * 1024 * 1024 * 5,
+    1024 * 2,
+    1024 * 3,
+    1,
+  }};
+
+  for (size_t index = 0; index < teststrings.size(); ++index) {
+    std::string const &teststr = teststrings[index];
+    int64_t const &exp         = expvals[index];
+    int64_t const got          = Config::bytesFrom(teststr.c_str());
+
+    CHECK(got == exp);
+    if (got != exp) {
+      INFO(teststr.c_str());
+    }
+  }
+}
+
+TEST_CASE("config bytesfrom invalid parsing", "[AWS][slice][utility]")
+{
+  static std::array<std::string, 5> const badstrings = {{
+    "abc",  // alpha
+    "g00",  // giga
+    "M00",  // mega
+    "k00",  // kilo
+    "-500", // negative
+  }};
+
+  for (std::string const &badstr : badstrings) {
+    int64_t const val = Config::bytesFrom(badstr.c_str());
+    CHECK(0 == val);
+    if (0 != val) {
+      INFO(badstr.c_str());
+    }
+  }
+}
diff --git a/plugins/experimental/slice/unit-tests/test_content_range.cc b/plugins/experimental/slice/unit-tests/test_content_range.cc
new file mode 100644
index 0000000..a987629
--- /dev/null
+++ b/plugins/experimental/slice/unit-tests/test_content_range.cc
@@ -0,0 +1,80 @@
+/** @file
+  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.
+ */
+
+/**
+ * @file test_content_range.cc
+ * @brief Unit test for slice ContentRange
+ */
+
+#define CATCH_CONFIG_MAIN /* include main function */
+#include "catch.hpp"      /* catch unit-test framework */
+#include "../ContentRange.h"
+
+TEST_CASE("content_range invalid state", "[AWS][slice][utility]")
+{
+  CHECK_FALSE(ContentRange().isValid());                 // null range
+  CHECK_FALSE(ContentRange(1024, 1024, 4000).isValid()); // zero range
+  CHECK_FALSE(ContentRange(0, 1024, 1023).isValid());    // past end
+  CHECK_FALSE(ContentRange(-5, 13, 40).isValid());       // negative start
+}
+
+TEST_CASE("content_range to/from string - valid", "[AWS][slice][utility]")
+{
+  ContentRange const exprange(1023, 1048576, 307232768);
+
+  CHECK(exprange.isValid());
+
+  std::string const expstr("bytes 1023-1048575/307232768");
+
+  char gotbuf[1024];
+  int gotlen = sizeof(gotbuf);
+
+  bool const strstat(exprange.toStringClosed(gotbuf, &gotlen));
+
+  CHECK(strstat);
+  CHECK(gotlen == expstr.size());
+  CHECK(expstr == std::string(gotbuf));
+
+  ContentRange gotrange;
+  bool const gotstat(gotrange.fromStringClosed(expstr.c_str()));
+
+  CHECK(gotstat);
+  CHECK(gotrange.m_beg == exprange.m_beg);
+  CHECK(gotrange.m_end == exprange.m_end);
+  CHECK(gotrange.m_length == exprange.m_length);
+}
+
+TEST_CASE("content_range from string - invalid", "[AWS][slice][utility]")
+{
+  std::vector<std::string> const badstrings = {
+    "bytes=1024-1692",              // malformed
+    "bytes=1023-1048575/307232768", // malformed
+    "bytes 1023-1022/5000",         // zero size
+    "bytes -40-12/50",              // negative start
+    "bytes 5-13/11"                 // past end
+  };
+
+  ContentRange cr;
+
+  for (std::string const &badstr : badstrings) {
+    if (!cr.fromStringClosed(badstr.c_str())) {
+      CHECK_FALSE(cr.isValid());
+      INFO(badstr.c_str());
+    }
+  }
+}
diff --git a/plugins/experimental/slice/unit-tests/test_range.cc b/plugins/experimental/slice/unit-tests/test_range.cc
new file mode 100644
index 0000000..d2b1813
--- /dev/null
+++ b/plugins/experimental/slice/unit-tests/test_range.cc
@@ -0,0 +1,99 @@
+/** @file
+  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.
+ */
+
+/**
+ * @file test_content_range.cc
+ * @brief Unit test for slice ContentRange
+ */
+
+#define CATCH_CONFIG_MAIN /* include main function */
+#include "catch.hpp"      /* catch unit-test framework */
+#include "../Range.h"
+
+TEST_CASE("range invalid state", "[AWS][slice][utility]")
+{
+  CHECK_FALSE(Range().isValid());           // null range
+  CHECK_FALSE(Range(1024, 1024).isValid()); // zero range
+  CHECK_FALSE(Range(-5, 13).isValid());     // negative start
+}
+
+TEST_CASE("range to/from string - valid", "[AWS][slice][utility]")
+{
+  std::vector<std::string> const teststrings = {
+    "bytes=0-1023", // start at zero
+    "bytes=1-1024", // start from non zero
+    "bytes=11-11",  // single byte
+    "bytes=1-",     // 2nd byte to end
+    "bytes=3-17",   // ,23-29" // open
+    "bytes=3 -17 ", //,18-29" // adjacent
+    "bytes=3- 17",  //, 11-29" // overlapping
+    "bytes=3 - 11", //,13-17 , 23-29" // unsorted triplet
+    "bytes=3-11 ",  //,13-17, 23-29" // unsorted triplet
+    "bytes=0-0",    //,-1" // first and last bytes
+    "bytes=-20",    // last 20 bytes of file
+  };
+
+  std::vector<Range> const exps = {
+    Range{0, 1023 + 1},      //
+    Range{1, 1024 + 1},      //
+    Range{11, 11 + 1},       //
+    Range{1, Range::maxval}, //
+    Range{3, 17 + 1},        //
+    Range{3, 17 + 1},        //
+    Range{3, 17 + 1},        //
+    Range{3, 11 + 1},        //
+    Range{3, 11 + 1},        //
+    Range{0, 1},             //
+    Range{-20, 0}            //
+  };
+
+  for (size_t index = 0; index < teststrings.size(); ++index) {
+    std::string const &str = teststrings[index];
+
+    Range got;
+    CHECK(got.fromStringClosed(str.c_str()));
+    CHECK(got.isValid());
+
+    if (!got.isValid()) {
+      INFO(str.c_str());
+    }
+
+    Range const &exp = exps[index];
+    CHECK(got.m_beg == exp.m_beg);
+    CHECK(got.m_end == exp.m_end);
+  }
+}
+
+TEST_CASE("range from string - invalid")
+{
+  std::vector<std::string> const badstrings = {
+    "Range: bytes=-13",   // malformed
+    "bytes=-60-50",       // first negative, second nonzero
+    "bytes=17-13",        // degenerate
+    "bytes 0-1023/146515" // malformed
+  };
+
+  Range range;
+  for (std::string const &badstr : badstrings) {
+    CHECK_FALSE(range.fromStringClosed(badstr.c_str()));
+    CHECK_FALSE(range.isValid());
+    if (range.isValid()) {
+      INFO(badstr.c_str());
+    }
+  }
+}
diff --git a/plugins/experimental/slice/util.cc b/plugins/experimental/slice/util.cc
new file mode 100644
index 0000000..f3ae7d2
--- /dev/null
+++ b/plugins/experimental/slice/util.cc
@@ -0,0 +1,134 @@
+/** @file
+  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 "util.h"
+
+#include "Config.h"
+#include "Data.h"
+
+void
+shutdown(TSCont const contp, Data *const data)
+{
+  DEBUG_LOG("shutting down transaction");
+  TSContDataSet(contp, nullptr);
+  delete data;
+  TSContDestroy(contp);
+}
+
+void
+abort(TSCont const contp, Data *const data)
+{
+  DEBUG_LOG("aborting transaction");
+  TSContDataSet(contp, nullptr);
+  data->m_upstream.abort();
+  data->m_dnstream.abort();
+  delete data;
+  TSContDestroy(contp);
+}
+
+// create and issue a block request
+bool
+request_block(TSCont contp, Data *const data)
+{
+  // ensure no upstream connection
+  if (data->m_upstream.m_read.isOpen()) {
+    ERROR_LOG("Block request already in flight!");
+    return false;
+  }
+
+  if (Data::BlockState::Pending != data->m_blockstate) {
+    ERROR_LOG("request_block called with non Pending state!");
+    return false;
+  }
+
+  int64_t const blockbeg = (data->m_config->m_blockbytes * data->m_blocknum);
+  Range blockbe(blockbeg, blockbeg + data->m_config->m_blockbytes);
+
+  char rangestr[1024];
+  int rangelen      = sizeof(rangestr);
+  bool const rpstat = blockbe.toStringClosed(rangestr, &rangelen);
+  TSAssert(rpstat);
+
+  DEBUG_LOG("requestBlock: %s", rangestr);
+
+  // reuse the incoming client header, just change the range
+  HttpHeader header(data->m_req_hdrmgr.m_buffer, data->m_req_hdrmgr.m_lochdr);
+
+  // add/set sub range key and add slicer tag
+  bool const rangestat = header.setKeyVal(TS_MIME_FIELD_RANGE, TS_MIME_LEN_RANGE, rangestr, rangelen);
+
+  if (!rangestat) {
+    ERROR_LOG("Error trying to set range request header %s", rangestr);
+    return false;
+  }
+
+  // create virtual connection back into ATS
+  TSVConn const upvc = TSHttpConnectWithPluginId((sockaddr *)&data->m_client_ip, PLUGIN_NAME, 0);
+
+  int const hlen = TSHttpHdrLengthGet(header.m_buffer, header.m_lochdr);
+
+  // set up connection with the HttpConnect server
+  data->m_upstream.setupConnection(upvc);
+  data->m_upstream.setupVioWrite(contp, hlen);
+
+  // Send full request
+  TSHttpHdrPrint(header.m_buffer, header.m_lochdr, data->m_upstream.m_write.m_iobuf);
+  TSVIOReenable(data->m_upstream.m_write.m_vio);
+
+  /*
+          std::string const headerstr(header.toString());
+          DEBUG_LOG("Headers\n%s", headerstr.c_str());
+  */
+
+  // get ready for data back from the server
+  data->m_upstream.setupVioRead(contp, INT64_MAX);
+
+  // anticipate the next server response header
+  TSHttpParserClear(data->m_http_parser);
+  data->m_resp_hdrmgr.resetHeader();
+
+  data->m_blockexpected              = 0;
+  data->m_blockconsumed              = 0;
+  data->m_blockstate                 = Data::BlockState::Active;
+  data->m_server_block_header_parsed = false;
+
+  return true;
+}
+
+bool
+reader_avail_more_than(TSIOBufferReader const reader, int64_t bytes)
+{
+  TSIOBufferBlock block = TSIOBufferReaderStart(reader);
+
+  if (nullptr == block) {
+    return false;
+  }
+
+  while (nullptr != block) {
+    int64_t const blockbytes = TSIOBufferBlockReadAvail(block, reader);
+    if (bytes < blockbytes) {
+      return true;
+    } else {
+      bytes -= blockbytes;
+    }
+
+    block = TSIOBufferBlockNext(block);
+  }
+
+  return false;
+}
diff --git a/plugins/experimental/slice/util.h b/plugins/experimental/slice/util.h
new file mode 100644
index 0000000..9da6f36
--- /dev/null
+++ b/plugins/experimental/slice/util.h
@@ -0,0 +1,36 @@
+/** @file
+  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 Data;
+
+/** Functions to deal with the connection to the client.
+ * Body content transfers are handled by the client.
+ * New block requests are also initiated by the client.
+ */
+
+void shutdown(TSCont const contp, Data *const data);
+
+void abort(TSCont const contp, Data *const data);
+
+bool request_block(TSCont contp, Data *const data);
+
+bool reader_avail_more_than(TSIOBufferReader const reader, int64_t bytes);
diff --git a/tests/gold_tests/pluginTest/slice/curlsort.sh b/tests/gold_tests/pluginTest/slice/curlsort.sh
new file mode 100755
index 0000000..eabf561
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/curlsort.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+#  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.
+
+cmd='curl'
+for arg in "$@"; do
+	case "$arg" in
+		*\'*)
+#			arg=`printf '%s' "$arg" | sed s/'/'\"'\"'/g"`
+			arg=`printf '%s' "$arg"`
+			;;
+		*) : ;;
+	esac
+	cmd="$cmd '$arg'"
+done
+
+cmd="$cmd -s -D /dev/stdout -o /dev/stderr"
+
+eval " $cmd" | sort
diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_200.stderr.gold b/tests/gold_tests/pluginTest/slice/gold/slice_200.stderr.gold
new file mode 100644
index 0000000..24ad29c
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold/slice_200.stderr.gold
@@ -0,0 +1 @@
+lets go surfin now``
diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_200.stdout.gold b/tests/gold_tests/pluginTest/slice/gold/slice_200.stdout.gold
new file mode 100644
index 0000000..50eeb38
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold/slice_200.stdout.gold
@@ -0,0 +1,8 @@
+``
+Cache-Control: ``
+Connection: ``
+Content-Length: 18
+Date: ``
+Etag: "path"
+HTTP/1.1 200 OK
+Server: ``
diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_206.stderr.gold b/tests/gold_tests/pluginTest/slice/gold/slice_206.stderr.gold
new file mode 100644
index 0000000..24ad29c
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold/slice_206.stderr.gold
@@ -0,0 +1 @@
+lets go surfin now``
diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_206.stdout.gold b/tests/gold_tests/pluginTest/slice/gold/slice_206.stdout.gold
new file mode 100644
index 0000000..0ff7343
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold/slice_206.stdout.gold
@@ -0,0 +1,9 @@
+``
+Cache-Control: ``
+Connection: ``
+Content-Length: 18
+Content-Range: bytes 0-17/18
+Date: ``
+Etag: "path"
+HTTP/1.1 206 Partial Content
+Server: ``
diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_first.stderr.gold b/tests/gold_tests/pluginTest/slice/gold/slice_first.stderr.gold
new file mode 100644
index 0000000..10893bb
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold/slice_first.stderr.gold
@@ -0,0 +1 @@
+lets go``
diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_first.stdout.gold b/tests/gold_tests/pluginTest/slice/gold/slice_first.stdout.gold
new file mode 100644
index 0000000..c12d4c1
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold/slice_first.stdout.gold
@@ -0,0 +1,9 @@
+``
+Cache-Control: max-age=``
+Connection: ``
+Content-Length: 7
+Content-Range: bytes 0-6/18
+Date: ``
+Etag: ``
+HTTP/1.1 206 Partial Content
+Server: ``
diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_last.stderr.gold b/tests/gold_tests/pluginTest/slice/gold/slice_last.stderr.gold
new file mode 100644
index 0000000..30d9d27
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold/slice_last.stderr.gold
@@ -0,0 +1 @@
+ now``
diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_last.stdout.gold b/tests/gold_tests/pluginTest/slice/gold/slice_last.stdout.gold
new file mode 100644
index 0000000..8457c95
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold/slice_last.stdout.gold
@@ -0,0 +1,9 @@
+``
+Cache-Control: max-age=``
+Connection: ``
+Content-Length: 4
+Content-Range: bytes 14-17/18
+Date: ``
+Etag: ``
+HTTP/1.1 206 Partial Content
+Server: ``
diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_mid.stderr.gold b/tests/gold_tests/pluginTest/slice/gold/slice_mid.stderr.gold
new file mode 100644
index 0000000..ef40e32
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold/slice_mid.stderr.gold
@@ -0,0 +1 @@
+go surfin no``
diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_mid.stdout.gold b/tests/gold_tests/pluginTest/slice/gold/slice_mid.stdout.gold
new file mode 100644
index 0000000..e078833
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold/slice_mid.stdout.gold
@@ -0,0 +1,9 @@
+``
+Cache-Control: max-age=``
+Connection: ``
+Content-Length: 12
+Content-Range: bytes 5-16/18
+Date: ``
+Etag: ``
+HTTP/1.1 206 Partial Content
+Server: ``
diff --git a/tests/gold_tests/pluginTest/slice/gold_error/crr.stderr.gold b/tests/gold_tests/pluginTest/slice/gold_error/crr.stderr.gold
new file mode 100644
index 0000000..fad0575
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold_error/crr.stderr.gold
@@ -0,0 +1 @@
+the quick``
diff --git a/tests/gold_tests/pluginTest/slice/gold_error/crr.stdout.gold b/tests/gold_tests/pluginTest/slice/gold_error/crr.stdout.gold
new file mode 100644
index 0000000..7109c04
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold_error/crr.stdout.gold
@@ -0,0 +1,9 @@
+``
+Cache-Control: ``
+Connection: ``
+Content-Length: 19
+Date: ``
+Etag: ``
+HTTP/1.1 200 OK
+Server: ``
+Via: ``
diff --git a/tests/gold_tests/pluginTest/slice/gold_error/etag.stderr.gold b/tests/gold_tests/pluginTest/slice/gold_error/etag.stderr.gold
new file mode 100644
index 0000000..fad0575
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold_error/etag.stderr.gold
@@ -0,0 +1 @@
+the quick``
diff --git a/tests/gold_tests/pluginTest/slice/gold_error/etag.stdout.gold b/tests/gold_tests/pluginTest/slice/gold_error/etag.stdout.gold
new file mode 100644
index 0000000..7109c04
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold_error/etag.stdout.gold
@@ -0,0 +1,9 @@
+``
+Cache-Control: ``
+Connection: ``
+Content-Length: 19
+Date: ``
+Etag: ``
+HTTP/1.1 200 OK
+Server: ``
+Via: ``
diff --git a/tests/gold_tests/pluginTest/slice/gold_error/lm.stderr.gold b/tests/gold_tests/pluginTest/slice/gold_error/lm.stderr.gold
new file mode 100644
index 0000000..fad0575
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold_error/lm.stderr.gold
@@ -0,0 +1 @@
+the quick``
diff --git a/tests/gold_tests/pluginTest/slice/gold_error/lm.stdout.gold b/tests/gold_tests/pluginTest/slice/gold_error/lm.stdout.gold
new file mode 100644
index 0000000..20bd1a5
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold_error/lm.stdout.gold
@@ -0,0 +1,9 @@
+``
+Cache-Control: ``
+Connection: ``
+Content-Length: 19
+Date: ``
+HTTP/1.1 200 OK
+Last-Modified: ``
+Server: ``
+Via: ``
diff --git a/tests/gold_tests/pluginTest/slice/gold_error/non206.stderr.gold b/tests/gold_tests/pluginTest/slice/gold_error/non206.stderr.gold
new file mode 100644
index 0000000..fad0575
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold_error/non206.stderr.gold
@@ -0,0 +1 @@
+the quick``
diff --git a/tests/gold_tests/pluginTest/slice/gold_error/non206.stdout.gold b/tests/gold_tests/pluginTest/slice/gold_error/non206.stdout.gold
new file mode 100644
index 0000000..f381661
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/gold_error/non206.stdout.gold
@@ -0,0 +1,10 @@
+``
+Cache-Control: ``
+Connection: ``
+Content-Length: 19
+Date: ``
+Etag: ``
+HTTP/1.1 200 OK
+Last-Modified: ``
+Server: ``
+Via: ``
diff --git a/tests/gold_tests/pluginTest/slice/slice.test.py b/tests/gold_tests/pluginTest/slice/slice.test.py
new file mode 100644
index 0000000..81e1f56
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/slice.test.py
@@ -0,0 +1,183 @@
+'''
+'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+Test.Summary = '''
+Basic slice plugin test
+'''
+
+## Test description:
+# Preload the cache with the entire asset to be range requested.
+# Reload remap rule with slice plugin
+# Request content through the slice plugin
+
+Test.SkipUnless(
+    Condition.PluginExists('slice.so'),
+)
+Test.ContinueOnFail = False
+
+# configure origin server
+server = Test.MakeOriginServer("server")
+
+# Define ATS and configure
+ts = Test.MakeATSProcess("ts", command="traffic_manager", select_ports=True)
+
+# default root
+request_header_chk = {"headers":
+  "GET / HTTP/1.1\r\n" +
+  "Host: www.example.com\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": "",
+}
+
+response_header_chk = {"headers":
+  "HTTP/1.1 200 OK\r\n" +
+  "Connection: close\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": "",
+}
+
+server.addResponse("sessionlog.json", request_header_chk, response_header_chk)
+
+#block_bytes = 7
+body = "lets go surfin now"
+
+request_header = {"headers":
+  "GET /path HTTP/1.1\r\n" +
+  "Host: www.example.com\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": "",
+}
+
+response_header = {"headers":
+  "HTTP/1.1 200 OK\r\n" +
+  "Connection: close\r\n" +
+  'Etag: "path"\r\n' +
+  "Cache-Control: max-age=500\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": body,
+}
+
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.Setup.CopyAs('curlsort.sh', Test.RunDirectory)
+curl_and_args = 'sh curlsort.sh -H "Host: www.example.com"'
+
+# set up whole asset fetch into cache
+ts.Disk.remap_config.AddLine(
+    'map / http://127.0.0.1:{}'.format(server.Variables.Port)
+)
+
+# minimal configuration
+ts.Disk.records_config.update({
+  'proxy.config.diags.debug.enabled': 1,
+  'proxy.config.diags.debug.tags': 'slice',
+  'proxy.config.http.cache.http': 1,
+  'proxy.config.http.wait_for_cache': 1,
+  'proxy.config.http.insert_age_in_response': 0,
+  'proxy.config.http.response_via_str': 3,
+})
+
+# 0 Test - Prefetch entire asset into cache
+tr = Test.AddTestRun("Fetch first slice range")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts)
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/path'.format(ts.Variables.port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/slice_200.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold/slice_200.stderr.gold"
+tr.StillRunningAfter = ts
+
+block_bytes = 7
+
+# 1 - Reconfigure remap.config with slice plugin
+tr = Test.AddTestRun("Load Slice plugin")
+remap_config_path = ts.Disk.remap_config.Name
+tr.Disk.File(remap_config_path, typename="ats:config").AddLines([
+  'map / http://127.0.0.1:{}'.format(server.Variables.Port) +
+    ' @plugin=slice.so @pparam=--blockbytes-test={}'.format(block_bytes)
+])
+
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'traffic_ctl config reload'
+# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket
+tr.Processes.Default.Env = ts.Env
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
+
+# 2 Test - First complete slice
+tr = Test.AddTestRun("Fetch first slice range")
+tr.DelayStart = 5
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/path'.format(ts.Variables.port) + ' -r 0-6'
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/slice_first.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold/slice_first.stderr.gold"
+tr.StillRunningAfter = ts
+
+# 3 Test - Last slice auto
+tr = Test.AddTestRun("Last slice -- 14-")
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/path'.format(ts.Variables.port) + ' -r 14-'
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/slice_last.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold/slice_last.stderr.gold"
+tr.StillRunningAfter = ts
+
+# 4 Test - Last slice exact
+tr = Test.AddTestRun("Last slice 14-17")
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/path'.format(ts.Variables.port) + ' -r 14-17'
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/slice_last.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold/slice_last.stderr.gold"
+tr.StillRunningAfter = ts
+
+# 5 Test - Last slice truncated
+tr = Test.AddTestRun("Last truncated slice 14-20")
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/path'.format(ts.Variables.port) + ' -r 14-20'
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/slice_last.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold/slice_last.stderr.gold"
+tr.StillRunningAfter = ts
+
+# 6 Test - Whole asset via slices
+tr = Test.AddTestRun("Whole asset via slices")
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/path'.format(ts.Variables.port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/slice_200.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold/slice_200.stderr.gold"
+tr.StillRunningAfter = ts
+
+# 7 Test - Whole asset via range
+tr = Test.AddTestRun("Whole asset via range")
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/path'.format(ts.Variables.port) + ' -r 0-'
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/slice_206.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold/slice_206.stderr.gold"
+tr.StillRunningAfter = ts
+
+# 8 Test - Non aligned slice request
+tr = Test.AddTestRun("Non aligned slice request")
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/path'.format(ts.Variables.port) + ' -r 5-16'
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/slice_mid.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold/slice_mid.stderr.gold"
+tr.StillRunningAfter = ts
diff --git a/tests/gold_tests/pluginTest/slice/slice_error.test.py b/tests/gold_tests/pluginTest/slice/slice_error.test.py
new file mode 100644
index 0000000..0ae06f1
--- /dev/null
+++ b/tests/gold_tests/pluginTest/slice/slice_error.test.py
@@ -0,0 +1,322 @@
+'''
+'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+Test.Summary = '''
+Slice plugin error.log test
+'''
+
+## Test description:
+# Preload the cache with the entire asset to be range requested.
+# Reload remap rule with slice plugin
+# Request content through the slice plugin
+
+Test.SkipUnless(
+    Condition.PluginExists('slice.so'),
+)
+Test.ContinueOnFail = False
+
+# configure origin server
+server = Test.MakeOriginServer("server", lookup_key="{%Range}{PATH}")
+
+# Define ATS and configure
+ts = Test.MakeATSProcess("ts", command="traffic_manager", select_ports=True)
+
+body = "the quick brown fox" # len 19
+
+# default root
+request_header_chk = {"headers":
+  "GET / HTTP/1.1\r\n" +
+  "Host: www.example.com\r\n" +
+  "Range: bytes=0-\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": "",
+}
+
+response_header_chk = {"headers":
+  "HTTP/1.1 206 Partial Content\r\n" +
+  "Connection: close\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": body,
+}
+
+server.addResponse("sessionlog.json", request_header_chk, response_header_chk)
+
+blockbytes = 9
+
+range0 = "{}-{}".format(0, blockbytes - 1)
+range1 = "{}-{}".format(blockbytes, (2 * blockbytes) - 1)
+
+body0 = body[0:blockbytes]
+body1 = body[blockbytes:2 * blockbytes]
+
+# Mismatch etag
+
+request_header_etag0 = {"headers":
+  "GET /etag HTTP/1.1\r\n" +
+  "Host: www.example.com\r\n" +
+  "Range: bytes={}\r\n".format(range0) +
+  "X-Slicer-Info: full content request\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": "",
+}
+
+response_header_etag0 = {"headers":
+  "HTTP/1.1 206 Partial Content\r\n" +
+  "Connection: close\r\n" +
+  'Etag: "etag0"\r\n' +
+  "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) +
+  "Cache-Control: max-age=500\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": body0,
+}
+
+server.addResponse("sessionlog.json", request_header_etag0, response_header_etag0)
+
+request_header_etag1 = {"headers":
+  "GET /etag HTTP/1.1\r\n" +
+  "Host: www.example.com\r\n" +
+  "Range: bytes={}\r\n".format(range1) +
+  "X-Slicer-Info: full content request\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": "",
+}
+
+response_header_etag1 = {"headers":
+  "HTTP/1.1 206 Partial Content\r\n" +
+  "Connection: close\r\n" +
+  'Etag: "etag1"\r\n' +
+  "Content-Range: bytes {}/{}\r\n".format(range1, len(body)) +
+  "Cache-Control: max-age=500\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": body1,
+}
+
+server.addResponse("sessionlog.json", request_header_etag1, response_header_etag1)
+
+# mismatch Last-Modified
+
+request_header_lm0 = {"headers":
+  "GET /lastmodified HTTP/1.1\r\n" +
+  "Host: www.example.com\r\n" +
+  "Range: bytes={}\r\n".format(range0) +
+  "X-Slicer-Info: full content request\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": "",
+}
+
+response_header_lm0 = {"headers":
+  "HTTP/1.1 206 Partial Content\r\n" +
+  "Connection: close\r\n" +
+  "Last-Modified: Tue, 08 May 2018 15:49:41 GMT\r\n" +
+  "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) +
+  "Cache-Control: max-age=500\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": body0,
+}
+
+server.addResponse("sessionlog.json", request_header_lm0, response_header_lm0)
+
+request_header_lm1 = {"headers":
+  "GET /lastmodified HTTP/1.1\r\n" +
+  "Host: www.example.com\r\n" +
+  "Range: bytes={}\r\n".format(range1) +
+  "X-Slicer-Info: full content request\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": "",
+}
+
+response_header_lm1 = {"headers":
+  "HTTP/1.1 206 Partial Content\r\n" +
+  "Connection: close\r\n" +
+  "Last-Modified: Tue, 08 Apr 2019 18:00:00 GMT\r\n" +
+  "Content-Range: bytes {}/{}\r\n".format(range1, len(body)) +
+  "Cache-Control: max-age=500\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": body1,
+}
+
+server.addResponse("sessionlog.json", request_header_lm1, response_header_lm1)
+
+# non 206 slice block
+
+request_header_n206_0 = {"headers":
+  "GET /non206 HTTP/1.1\r\n" +
+  "Host: www.example.com\r\n" +
+  "Range: bytes={}\r\n".format(range0) +
+  "X-Slicer-Info: full content request\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": "",
+}
+
+response_header_n206_0 = {"headers":
+  "HTTP/1.1 206 Partial Content\r\n" +
+  "Connection: close\r\n" +
+  'Etag: "etag"\r\n' +
+  "Last-Modified: Tue, 08 May 2018 15:49:41 GMT\r\n" +
+  "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) +
+  "Cache-Control: max-age=500\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": body0,
+}
+
+server.addResponse("sessionlog.json", request_header_n206_0, response_header_n206_0)
+
+# mismatch content-range
+
+request_header_crr0 = {"headers":
+  "GET /crr HTTP/1.1\r\n" +
+  "Host: www.example.com\r\n" +
+  "Range: bytes={}\r\n".format(range0) +
+  "X-Slicer-Info: full content request\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": "",
+}
+
+response_header_crr0 = {"headers":
+  "HTTP/1.1 206 Partial Content\r\n" +
+  "Connection: close\r\n" +
+  "Etag: crr\r\n" +
+  "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) +
+  "Cache-Control: max-age=500\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": body0,
+}
+
+server.addResponse("sessionlog.json", request_header_crr0, response_header_crr0)
+
+request_header_crr1 = {"headers":
+  "GET /crr HTTP/1.1\r\n" +
+  "Host: www.example.com\r\n" +
+  "Range: bytes={}\r\n".format(range1) +
+  "X-Slicer-Info: full content request\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": "",
+}
+
+response_header_crr1 = {"headers":
+  "HTTP/1.1 206 Partial Content\r\n" +
+  "Connection: close\r\n" +
+  "Etag: crr\r\n" +
+  "Content-Range: bytes {}/{}\r\n".format(range1, len(body) - 1) +
+  "Cache-Control: max-age=500\r\n" +
+  "\r\n",
+  "timestamp": "1469733493.993",
+  "body": body1,
+}
+
+server.addResponse("sessionlog.json", request_header_crr1, response_header_crr1)
+
+ts.Setup.CopyAs('curlsort.sh', Test.RunDirectory)
+curl_and_args = 'sh curlsort.sh -H "Host: www.example.com"'
+
+# set up whole asset fetch into cache
+ts.Disk.remap_config.AddLine(
+  'map / http://127.0.0.1:{}'.format(server.Variables.Port) +
+    ' @plugin=slice.so @pparam=--blockbytes-test={}'.format(blockbytes)
+)
+
+# minimal configuration
+ts.Disk.records_config.update({
+  'proxy.config.diags.debug.enabled': 0,
+  'proxy.config.diags.debug.tags': 'slice',
+  'proxy.config.http.cache.http': 0,
+  'proxy.config.http.wait_for_cache': 0,
+  'proxy.config.http.insert_age_in_response': 0,
+  'proxy.config.http.insert_request_via_str': 0,
+  'proxy.config.http.insert_response_via_str': 3,
+})
+
+# Override builtin error check as these cases will fail
+# taken from the slice plug code
+ts.Disk.diags_log.Content = Testers.ContainsExpression('reason="Mismatch block Etag"', "Mismatch block etag")
+ts.Disk.diags_log.Content += Testers.ContainsExpression('reason="Mismatch block Last-Modified"', "Mismatch block Last-Modified")
+ts.Disk.diags_log.Content += Testers.ContainsExpression('reason="Non 206 internal block response"', "Non 206 internal block response")
+ts.Disk.diags_log.Content += Testers.ContainsExpression('reason="Mismatch/Bad block Content-Range"', "Mismatch/Bad block Content-Range")
+
+# 0 Test - Etag mismatch test
+tr = Test.AddTestRun("Etag test")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts)
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/etag'.format(ts.Variables.port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold_error/etag.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold_error/etag.stderr.gold"
+tr.StillRunningAfter = ts
+
+# 1 Check - diags.log message
+tr = Test.AddTestRun("Etag error check")
+tr.Processes.Default.Command = "grep 'Mismatch block Etag' {}".format(ts.Disk.diags_log.Name)
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = ts
+
+# 2 Test - Last Modified mismatch test
+tr = Test.AddTestRun("Last-Modified test")
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/lastmodified'.format(ts.Variables.port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold_error/lm.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold_error/lm.stderr.gold"
+tr.StillRunningAfter = ts
+
+# 3 Check - diags.log message
+tr = Test.AddTestRun("Last-Modified error check")
+tr.Processes.Default.Command = "grep 'Mismatch block Last-Modified' {}".format(ts.Disk.diags_log.Name)
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = ts
+
+# 4 Test - Non 206 mismatch test
+tr = Test.AddTestRun("Non 206 test")
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/non206'.format(ts.Variables.port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold_error/non206.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold_error/non206.stderr.gold"
+tr.StillRunningAfter = ts
+
+# 3 Check - diags.log message
+tr = Test.AddTestRun("Non 206 error check")
+tr.Processes.Default.Command = "grep 'Non 206 internal block response' {}".format(ts.Disk.diags_log.Name)
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = ts
+
+# 4 Test - Block content-range
+tr = Test.AddTestRun("Content-Range test")
+tr.Processes.Default.Command = curl_and_args + ' http://127.0.0.1:{}/crr'.format(ts.Variables.port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold_error/crr.stdout.gold"
+tr.Processes.Default.Streams.stderr = "gold_error/crr.stderr.gold"
+tr.StillRunningAfter = ts
+
+# 3 Check - diags.log message
+tr = Test.AddTestRun("Content-Range error check")
+tr.Processes.Default.Command = "grep 'Mismatch/Bad block Content-Range' {}".format(ts.Disk.diags_log.Name)
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = ts