]> git.ipfire.org Git - thirdparty/squid.git/blobdiff - src/client_side_reply.cc
Source Format Enforcement (#763)
[thirdparty/squid.git] / src / client_side_reply.cc
index c777cfecd95b7696f20f08b0010148d6d0ae5157..1512772704ef526b913050eaf457db08016872d0 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 1996-2015 The Squid Software Foundation and contributors
+ * Copyright (C) 1996-2021 The Squid Software Foundation and contributors
  *
  * Squid software is distributed under GPLv2+ license and includes
  * contributions from numerous individuals and organizations.
@@ -20,7 +20,7 @@
 #include "format/Token.h"
 #include "FwdState.h"
 #include "globals.h"
-#include "globals.h"
+#include "http/Stream.h"
 #include "HttpHeaderTools.h"
 #include "HttpReply.h"
 #include "HttpRequest.h"
@@ -37,7 +37,6 @@
 #include "Store.h"
 #include "StrList.h"
 #include "tools.h"
-#include "URL.h"
 #if USE_AUTH
 #include "auth/UserRequest.h"
 #endif
@@ -54,7 +53,7 @@ CBDATA_CLASS_INIT(clientReplyContext);
 
 /* Local functions */
 extern "C" CSS clientReplyStatus;
-ErrorState *clientBuildError(err_type, Http::StatusCode, char const *, Ip::Address &, HttpRequest *);
+ErrorState *clientBuildError(err_type, Http::StatusCode, char const *, const ConnStateData *, HttpRequest *, const AccessLogEntry::Pointer &);
 
 /* privates */
 
@@ -75,21 +74,19 @@ clientReplyContext::~clientReplyContext()
 
 clientReplyContext::clientReplyContext(ClientHttpRequest *clientContext) :
     purgeStatus(Http::scNone),
-    lookingforstore(0),
     http(cbdataReference(clientContext)),
     headers_sz(0),
     sc(NULL),
     old_reqsize(0),
     reqsize(0),
     reqofs(0),
-#if USE_CACHE_DIGESTS
-    lookup_type(NULL),
-#endif
     ourNode(NULL),
     reply(NULL),
     old_entry(NULL),
     old_sc(NULL),
-    deleting(false)
+    old_lastmod(-1),
+    deleting(false),
+    collapsedRevalidation(crNone)
 {
     *tempbuf = 0;
 }
@@ -102,7 +99,7 @@ clientReplyContext::clientReplyContext(ClientHttpRequest *clientContext) :
 void
 clientReplyContext::setReplyToError(
     err_type err, Http::StatusCode status, const HttpRequestMethod& method, char const *uri,
-    Ip::Address &addr, HttpRequest * failedrequest, const char *unparsedrequest,
+    const ConnStateData *conn, HttpRequest *failedrequest, const char *unparsedrequest,
 #if USE_AUTH
     Auth::UserRequest::Pointer auth_user_request
 #else
@@ -110,7 +107,7 @@ clientReplyContext::setReplyToError(
 #endif
 )
 {
-    ErrorState *errstate = clientBuildError(err, status, uri, addr, failedrequest);
+    auto errstate = clientBuildError(err, status, uri, conn, failedrequest, http->al);
 
     if (unparsedrequest)
         errstate->request_hdrs = xstrdup(unparsedrequest);
@@ -203,6 +200,8 @@ clientReplyContext::saveState()
     debugs(88, 3, "clientReplyContext::saveState: saving store context");
     old_entry = http->storeEntry();
     old_sc = sc;
+    old_lastmod = http->request->lastmod;
+    old_etag = http->request->etag;
     old_reqsize = reqsize;
     tempBuffer.offset = reqofs;
     /* Prevent accessing the now saved entries */
@@ -222,9 +221,13 @@ clientReplyContext::restoreState()
     sc = old_sc;
     reqsize = old_reqsize;
     reqofs = tempBuffer.offset;
+    http->request->lastmod = old_lastmod;
+    http->request->etag = old_etag;
     /* Prevent accessed the old saved entries */
     old_entry = NULL;
     old_sc = NULL;
+    old_lastmod = -1;
+    old_etag.clean();
     old_reqsize = 0;
     tempBuffer.offset = 0;
 }
@@ -264,9 +267,9 @@ void
 clientReplyContext::processExpired()
 {
     const char *url = storeId();
-    StoreEntry *entry = NULL;
     debugs(88, 3, "clientReplyContext::processExpired: '" << http->uri << "'");
-    assert(http->storeEntry()->lastmod >= 0);
+    const time_t lastmod = http->storeEntry()->lastModified();
+    assert(lastmod >= 0);
     /*
      * check if we are allowed to contact other servers
      * @?@: Instead of a 504 (Gateway Timeout) reply, we may want to return
@@ -278,6 +281,7 @@ clientReplyContext::processExpired()
         return;
     }
 
+    http->logType.update(LOG_TCP_REFRESH);
     http->request->flags.refresh = true;
 #if STORE_CLIENT_LIST_DEBUG
     /* Prevent a race with the store client memory free routines
@@ -286,16 +290,52 @@ clientReplyContext::processExpired()
 #endif
     /* Prepare to make a new temporary request */
     saveState();
-    entry = storeCreateEntry(url,
-                             http->log_uri, http->request->flags, http->request->method);
-    /* NOTE, don't call StoreEntry->lock(), storeCreateEntry() does it */
+
+    // TODO: Consider also allowing regular (non-collapsed) revalidation hits.
+    // TODO: support collapsed revalidation for Vary-controlled entries
+    bool collapsingAllowed = Config.onoff.collapsed_forwarding &&
+                             !Store::Controller::SmpAware() &&
+                             http->request->vary_headers.isEmpty();
+
+    StoreEntry *entry = nullptr;
+    if (collapsingAllowed) {
+        if (const auto e = storeGetPublicByRequest(http->request, ksRevalidation)) {
+            if (e->hittingRequiresCollapsing() && startCollapsingOn(*e, true)) {
+                entry = e;
+                entry->lock("clientReplyContext::processExpired#alreadyRevalidating");
+            } else {
+                e->abandon(__FUNCTION__);
+                // assume mayInitiateCollapsing() would fail too
+                collapsingAllowed = false;
+            }
+        }
+    }
+
+    if (entry) {
+        entry->ensureMemObject(url, http->log_uri, http->request->method);
+        debugs(88, 5, "collapsed on existing revalidation entry: " << *entry);
+        collapsedRevalidation = crSlave;
+    } else {
+        entry = storeCreateEntry(url,
+                                 http->log_uri, http->request->flags, http->request->method);
+        /* NOTE, don't call StoreEntry->lock(), storeCreateEntry() does it */
+
+        if (collapsingAllowed && mayInitiateCollapsing() &&
+                Store::Root().allowCollapsing(entry, http->request->flags, http->request->method)) {
+            debugs(88, 5, "allow other revalidation requests to collapse on " << *entry);
+            collapsedRevalidation = crInitiator;
+        } else {
+            collapsedRevalidation = crNone;
+        }
+    }
+
     sc = storeClientListAdd(entry, this);
 #if USE_DELAY_POOLS
     /* delay_id is already set on original store client */
     sc->setDelayId(DelayId::DelayClient(http));
 #endif
 
-    http->request->lastmod = old_entry->lastmod;
+    http->request->lastmod = lastmod;
 
     if (!http->request->header.has(Http::HdrType::IF_NONE_MATCH)) {
         ETag etag = {NULL, -1}; // TODO: make that a default ETag constructor
@@ -303,18 +343,19 @@ clientReplyContext::processExpired()
             http->request->etag = etag.str;
     }
 
-    debugs(88, 5, "clientReplyContext::processExpired : lastmod " << entry->lastmod );
+    debugs(88, 5, "lastmod " << entry->lastModified());
     http->storeEntry(entry);
     assert(http->out.offset == 0);
     assert(http->request->clientConnectionManager == http->getConn());
 
-    /*
-     * A refcounted pointer so that FwdState stays around as long as
-     * this clientReplyContext does
-     */
-    Comm::ConnectionPointer conn = http->getConn() != NULL ? http->getConn()->clientConnection : NULL;
-    FwdState::Start(conn, http->storeEntry(), http->request, http->al);
-
+    if (collapsedRevalidation != crSlave) {
+        /*
+         * A refcounted pointer so that FwdState stays around as long as
+         * this clientReplyContext does
+         */
+        Comm::ConnectionPointer conn = http->getConn() != NULL ? http->getConn()->clientConnection : NULL;
+        FwdState::Start(conn, http->storeEntry(), http->request, http->al);
+    }
     /* Register with storage manager to receive updates when data comes in. */
 
     if (EBIT_TEST(entry->flags, ENTRY_ABORTED))
@@ -332,6 +373,10 @@ clientReplyContext::sendClientUpstreamResponse()
 {
     StoreIOBuffer tempresult;
     removeStoreReference(&old_sc, &old_entry);
+
+    if (collapsedRevalidation)
+        http->storeEntry()->clearPublicKeyScope();
+
     /* here the data to send is the data we just received */
     tempBuffer.offset = 0;
     old_reqsize = 0;
@@ -377,7 +422,7 @@ clientReplyContext::handleIMSReply(StoreIOBuffer result)
     if (deleting)
         return;
 
-    debugs(88, 3, "handleIMSReply: " << http->storeEntry()->url() << ", " << (long unsigned) result.length << " bytes" );
+    debugs(88, 3, http->storeEntry()->url() << ", " << (long unsigned) result.length << " bytes");
 
     if (http->storeEntry() == NULL)
         return;
@@ -385,64 +430,85 @@ clientReplyContext::handleIMSReply(StoreIOBuffer result)
     if (result.flags.error && !EBIT_TEST(http->storeEntry()->flags, ENTRY_ABORTED))
         return;
 
+    if (collapsedRevalidation == crSlave && !http->storeEntry()->mayStartHitting()) {
+        debugs(88, 3, "CF slave hit private non-shareable " << *http->storeEntry() << ". MISS");
+        // restore context to meet processMiss() expectations
+        restoreState();
+        http->logType.update(LOG_TCP_MISS);
+        processMiss();
+        return;
+    }
+
     /* update size of the request */
     reqsize = result.length + reqofs;
 
-    const Http::StatusCode status = http->storeEntry()->getReply()->sline.status();
-
     // request to origin was aborted
     if (EBIT_TEST(http->storeEntry()->flags, ENTRY_ABORTED)) {
-        debugs(88, 3, "handleIMSReply: request to origin aborted '" << http->storeEntry()->url() << "', sending old entry to client" );
-        http->logType = LOG_TCP_REFRESH_FAIL_OLD;
+        debugs(88, 3, "request to origin aborted '" << http->storeEntry()->url() << "', sending old entry to client");
+        http->logType.update(LOG_TCP_REFRESH_FAIL_OLD);
         sendClientOldEntry();
+        return;
     }
 
-    HttpReply *old_rep = (HttpReply *) old_entry->getReply();
+    const auto oldStatus = old_entry->mem().freshestReply().sline.status();
+    const auto &new_rep = http->storeEntry()->mem().freshestReply();
+    const auto status = new_rep.sline.status();
+
+    // XXX: Disregard stale incomplete (i.e. still being written) borrowed (i.e.
+    // not caused by our request) IMS responses. That new_rep may be very old!
 
     // origin replied 304
     if (status == Http::scNotModified) {
-        http->logType = LOG_TCP_REFRESH_UNMODIFIED;
+        http->logType.update(LOG_TCP_REFRESH_UNMODIFIED);
         http->request->flags.staleIfHit = false; // old_entry is no longer stale
 
-        // update headers on existing entry
-        old_rep->updateOnNotModified(http->storeEntry()->getReply());
-        old_entry->timestampsSet();
+        // TODO: The update may not be instantaneous. Should we wait for its
+        // completion to avoid spawning too much client-disassociated work?
+        Store::Root().updateOnNotModified(old_entry, *http->storeEntry());
 
         // if client sent IMS
-
-        if (http->request->flags.ims && !old_entry->modifiedSince(http->request)) {
+        if (http->request->flags.ims && !old_entry->modifiedSince(http->request->ims, http->request->imslen)) {
             // forward the 304 from origin
-            debugs(88, 3, "handleIMSReply: origin replied 304, revalidating existing entry and forwarding 304 to client");
+            debugs(88, 3, "origin replied 304, revalidated existing entry and forwarding 304 to client");
             sendClientUpstreamResponse();
-        } else {
-            // send existing entry, it's still valid
-            debugs(88, 3, "handleIMSReply: origin replied 304, revalidating existing entry and sending " <<
-                   old_rep->sline.status() << " to client");
-            sendClientOldEntry();
+            return;
         }
+
+        // send existing entry, it's still valid
+        debugs(88, 3, "origin replied 304, revalidated existing entry and sending " << oldStatus << " to client");
+        sendClientOldEntry();
+        return;
     }
 
     // origin replied with a non-error code
-    else if (status > Http::scNone && status < Http::scInternalServerError) {
-        // forward response from origin
-        http->logType = LOG_TCP_REFRESH_MODIFIED;
-        debugs(88, 3, "handleIMSReply: origin replied " << status << ", replacing existing entry and forwarding to client");
+    if (status > Http::scNone && status < Http::scInternalServerError) {
+        // RFC 7234 section 4: a cache MUST use the most recent response
+        // (as determined by the Date header field)
+        if (new_rep.olderThan(&old_entry->mem().freshestReply())) {
+            http->logType.err.ignored = true;
+            debugs(88, 3, "origin replied " << status << " but with an older date header, sending old entry (" << oldStatus << ") to client");
+            sendClientOldEntry();
+            return;
+        }
+
+        http->logType.update(LOG_TCP_REFRESH_MODIFIED);
+        debugs(88, 3, "origin replied " << status << ", forwarding to client");
         sendClientUpstreamResponse();
+        return;
     }
 
     // origin replied with an error
-    else if (http->request->flags.failOnValidationError) {
-        http->logType = LOG_TCP_REFRESH_FAIL_ERR;
-        debugs(88, 3, "handleIMSReply: origin replied with error " << status <<
-               ", forwarding to client due to fail_on_validation_err");
+    if (http->request->flags.failOnValidationError) {
+        http->logType.update(LOG_TCP_REFRESH_FAIL_ERR);
+        debugs(88, 3, "origin replied with error " << status << ", forwarding to client due to fail_on_validation_err");
         sendClientUpstreamResponse();
-    } else {
-        // ignore and let client have old entry
-        http->logType = LOG_TCP_REFRESH_FAIL_OLD;
-        debugs(88, 3, "handleIMSReply: origin replied with error " <<
-               status << ", sending old entry (" << old_rep->sline.status() << ") to client");
-        sendClientOldEntry();
+        return;
     }
+
+    // ignore and let client have old entry
+    http->logType.update(LOG_TCP_REFRESH_FAIL_OLD);
+    debugs(88, 3, "origin replied with error " << status << ", sending old entry (" << oldStatus << ") to client");
+    sendClientOldEntry();
 }
 
 SQUIDCEXTERN CSR clientGetMoreData;
@@ -486,19 +552,29 @@ clientReplyContext::cacheHit(StoreIOBuffer result)
     } else if (result.flags.error) {
         /* swap in failure */
         debugs(88, 3, "clientCacheHit: swapin failure for " << http->uri);
-        http->logType = LOG_TCP_SWAPFAIL_MISS;
+        http->logType.update(LOG_TCP_SWAPFAIL_MISS);
         removeClientStoreReference(&sc, http);
         processMiss();
         return;
     }
 
+    // The previously identified hit suddenly became unshareable!
+    // This is common for collapsed forwarding slaves but might also
+    // happen to regular hits because we are called asynchronously.
+    if (!e->mayStartHitting()) {
+        debugs(88, 3, "unshareable " << *e << ". MISS");
+        http->logType.update(LOG_TCP_MISS);
+        processMiss();
+        return;
+    }
+
     if (result.length == 0) {
         debugs(88, 5, "store IO buffer has no content. MISS");
         /* the store couldn't get enough data from the file for us to id the
          * object
          */
         /* treat as a miss */
-        http->logType = LOG_TCP_MISS;
+        http->logType.update(LOG_TCP_MISS);
         processMiss();
         return;
     }
@@ -514,7 +590,7 @@ clientReplyContext::cacheHit(StoreIOBuffer result)
 
     if (http->request->storeId().cmp(e->mem_obj->storeId()) != 0) {
         debugs(33, DBG_IMPORTANT, "clientProcessHit: URL mismatch, '" << e->mem_obj->storeId() << "' != '" << http->request->storeId() << "'");
-        http->logType = LOG_TCP_MISS; // we lack a more precise LOG_*_MISS code
+        http->logType.update(LOG_TCP_MISS); // we lack a more precise LOG_*_MISS code
         processMiss();
         return;
     }
@@ -546,7 +622,7 @@ clientReplyContext::cacheHit(StoreIOBuffer result)
     case VARY_CANCEL:
         /* varyEvaluateMatch found a object loop. Process as miss */
         debugs(88, DBG_IMPORTANT, "clientProcessHit: Vary object loop!");
-        http->logType = LOG_TCP_MISS; // we lack a more precise LOG_*_MISS code
+        http->logType.update(LOG_TCP_MISS); // we lack a more precise LOG_*_MISS code
         processMiss();
         return;
     }
@@ -561,11 +637,12 @@ clientReplyContext::cacheHit(StoreIOBuffer result)
 
     if (e->checkNegativeHit() && !r->flags.noCacheHack()) {
         debugs(88, 5, "negative-HIT");
-        http->logType = LOG_TCP_NEGATIVE_HIT;
+        http->logType.update(LOG_TCP_NEGATIVE_HIT);
         sendMoreData(result);
+        return;
     } else if (blockedHit()) {
         debugs(88, 5, "send_hit forces a MISS");
-        http->logType = LOG_TCP_MISS;
+        http->logType.update(LOG_TCP_MISS);
         processMiss();
         return;
     } else if (!http->flags.internal && refreshCheckHTTP(e, r)) {
@@ -582,13 +659,14 @@ clientReplyContext::cacheHit(StoreIOBuffer result)
          */
         r->flags.needValidation = true;
 
-        if (e->lastmod < 0) {
-            debugs(88, 3, "validate HIT object? NO. Missing Last-Modified header. Do MISS.");
+        if (e->lastModified() < 0) {
+            debugs(88, 3, "validate HIT object? NO. Can't calculate entry modification time. Do MISS.");
             /*
-             * Previous reply didn't have a Last-Modified header,
-             * we cannot revalidate it.
+             * We cannot revalidate entries without knowing their
+             * modification time.
+             * XXX: BUG 1890 objects without Date do not get one added.
              */
-            http->logType = LOG_TCP_MISS;
+            http->logType.update(LOG_TCP_MISS);
             processMiss();
         } else if (r->flags.noCache) {
             debugs(88, 3, "validate HIT object? NO. Client sent CC:no-cache. Do CLIENT_REFRESH_MISS");
@@ -596,9 +674,9 @@ clientReplyContext::cacheHit(StoreIOBuffer result)
              * This did not match a refresh pattern that overrides no-cache
              * we should honour the client no-cache header.
              */
-            http->logType = LOG_TCP_CLIENT_REFRESH_MISS;
+            http->logType.update(LOG_TCP_CLIENT_REFRESH_MISS);
             processMiss();
-        } else if (r->url.getScheme() == AnyP::PROTO_HTTP) {
+        } else if (r->url.getScheme() == AnyP::PROTO_HTTP || r->url.getScheme() == AnyP::PROTO_HTTPS) {
             debugs(88, 3, "validate HIT object? YES.");
             /*
              * Object needs to be revalidated
@@ -611,30 +689,32 @@ clientReplyContext::cacheHit(StoreIOBuffer result)
              * We don't know how to re-validate other protocols. Handle
              * them as if the object has expired.
              */
-            http->logType = LOG_TCP_MISS;
+            http->logType.update(LOG_TCP_MISS);
             processMiss();
         }
+        return;
     } else if (r->conditional()) {
         debugs(88, 5, "conditional HIT");
-        processConditional(result);
-    } else {
-        /*
-         * plain ol' cache hit
-         */
-        debugs(88, 5, "plain old HIT");
+        if (processConditional(result))
+            return;
+    }
+
+    /*
+     * plain ol' cache hit
+     */
+    debugs(88, 5, "plain old HIT");
 
 #if USE_DELAY_POOLS
-        if (e->store_status != STORE_OK)
-            http->logType = LOG_TCP_MISS;
-        else
+    if (e->store_status != STORE_OK)
+        http->logType.update(LOG_TCP_MISS);
+    else
 #endif
-            if (e->mem_status == IN_MEMORY)
-                http->logType = LOG_TCP_MEM_HIT;
-            else if (Config.onoff.offline)
-                http->logType = LOG_TCP_OFFLINE_HIT;
+        if (e->mem_status == IN_MEMORY)
+            http->logType.update(LOG_TCP_MEM_HIT);
+        else if (Config.onoff.offline)
+            http->logType.update(LOG_TCP_OFFLINE_HIT);
 
-        sendMoreData(result);
-    }
+    sendMoreData(result);
 }
 
 /**
@@ -682,7 +762,7 @@ clientReplyContext::processMiss()
     /// Deny loops
     if (r->flags.loopDetected) {
         http->al->http.code = Http::scForbidden;
-        err = clientBuildError(ERR_ACCESS_DENIED, Http::scForbidden, NULL, http->getConn()->clientConnection->remote, http->request);
+        err = clientBuildError(ERR_ACCESS_DENIED, Http::scForbidden, nullptr, http->getConn(), http->request, http->al);
         createStoreEntry(r->method, RequestFlags());
         errorAppendEntry(http->storeEntry(), err);
         triggerInitialStoreRead();
@@ -693,8 +773,8 @@ clientReplyContext::processMiss()
         triggerInitialStoreRead();
 
         if (http->redirect.status) {
-            HttpReply *rep = new HttpReply;
-            http->logType = LOG_TCP_REDIRECT;
+            const HttpReplyPointer rep(new HttpReply);
+            http->logType.update(LOG_TCP_REDIRECT);
             http->storeEntry()->releaseRequest();
             rep->redirect(http->redirect.status, http->redirect.location);
             http->storeEntry()->replaceHttpReply(rep);
@@ -704,8 +784,8 @@ clientReplyContext::processMiss()
 
         assert(r->clientConnectionManager == http->getConn());
 
+        Comm::ConnectionPointer conn = http->getConn() != nullptr ? http->getConn()->clientConnection : nullptr;
         /** Start forwarding to get the new object from network */
-        Comm::ConnectionPointer conn = http->getConn() != NULL ? http->getConn()->clientConnection : NULL;
         FwdState::Start(conn, http->storeEntry(), r, http->al);
     }
 }
@@ -722,23 +802,23 @@ clientReplyContext::processOnlyIfCachedMiss()
     debugs(88, 4, http->request->method << ' ' << http->uri);
     http->al->http.code = Http::scGatewayTimeout;
     ErrorState *err = clientBuildError(ERR_ONLY_IF_CACHED_MISS, Http::scGatewayTimeout, NULL,
-                                       http->getConn()->clientConnection->remote, http->request);
+                                       http->getConn(), http->request, http->al);
     removeClientStoreReference(&sc, http);
     startError(err);
 }
 
 /// process conditional request from client
-void
+bool
 clientReplyContext::processConditional(StoreIOBuffer &result)
 {
     StoreEntry *const e = http->storeEntry();
 
-    if (e->getReply()->sline.status() != Http::scOkay) {
-        debugs(88, 4, "clientReplyContext::processConditional: Reply code " <<
-               e->getReply()->sline.status() << " != 200");
-        http->logType = LOG_TCP_MISS;
+    const auto replyStatusCode = e->mem().baseReply().sline.status();
+    if (replyStatusCode != Http::scOkay) {
+        debugs(88, 4, "miss because " << replyStatusCode << " != 200");
+        http->logType.update(LOG_TCP_MISS);
         processMiss();
-        return;
+        return true;
     }
 
     HttpRequest &r = *http->request;
@@ -746,51 +826,39 @@ clientReplyContext::processConditional(StoreIOBuffer &result)
     if (r.header.has(Http::HdrType::IF_MATCH) && !e->hasIfMatchEtag(r)) {
         // RFC 2616: reply with 412 Precondition Failed if If-Match did not match
         sendPreconditionFailedError();
-        return;
+        return true;
     }
 
-    bool matchedIfNoneMatch = false;
     if (r.header.has(Http::HdrType::IF_NONE_MATCH)) {
-        if (!e->hasIfNoneMatchEtag(r)) {
-            // RFC 2616: ignore IMS if If-None-Match did not match
-            r.flags.ims = false;
-            r.ims = -1;
-            r.imslen = 0;
-            r.header.delById(Http::HdrType::IF_MODIFIED_SINCE);
-            http->logType = LOG_TCP_MISS;
-            sendMoreData(result);
-            return;
-        }
+        // RFC 7232: If-None-Match recipient MUST ignore IMS
+        r.flags.ims = false;
+        r.ims = -1;
+        r.imslen = 0;
+        r.header.delById(Http::HdrType::IF_MODIFIED_SINCE);
 
-        if (!r.flags.ims) {
-            // RFC 2616: if If-None-Match matched and there is no IMS,
-            // reply with 304 Not Modified or 412 Precondition Failed
+        if (e->hasIfNoneMatchEtag(r)) {
             sendNotModifiedOrPreconditionFailedError();
-            return;
+            return true;
         }
 
-        // otherwise check IMS below to decide if we reply with 304 or 412
-        matchedIfNoneMatch = true;
+        // None-Match is true (no ETag matched); treat as an unconditional hit
+        return false;
     }
 
     if (r.flags.ims) {
         // handle If-Modified-Since requests from the client
-        if (e->modifiedSince(&r)) {
-            http->logType = LOG_TCP_IMS_HIT;
-            sendMoreData(result);
-            return;
-        }
+        if (e->modifiedSince(r.ims, r.imslen)) {
+            // Modified-Since is true; treat as an unconditional hit
+            return false;
 
-        if (matchedIfNoneMatch) {
-            // If-None-Match matched, reply with 304 Not Modified or
-            // 412 Precondition Failed
-            sendNotModifiedOrPreconditionFailedError();
-            return;
+        } else {
+            // otherwise reply with 304 Not Modified
+            sendNotModified();
         }
-
-        // otherwise reply with 304 Not Modified
-        sendNotModified();
+        return true;
     }
+
+    return false;
 }
 
 /// whether squid.conf send_hit prevents us from serving this hit
@@ -803,30 +871,13 @@ clientReplyContext::blockedHit() const
     if (http->flags.internal)
         return false; // internal content "hits" cannot be blocked
 
-    if (const HttpReply *rep = http->storeEntry()->getReply()) {
+    const auto &rep = http->storeEntry()->mem().freshestReply();
+    {
         std::unique_ptr<ACLFilledChecklist> chl(clientAclChecklistCreate(Config.accessList.sendHit, http));
-        chl->reply = const_cast<HttpReply*>(rep); // ACLChecklist API bug
+        chl->reply = const_cast<HttpReply*>(&rep); // ACLChecklist API bug
         HTTPMSGLOCK(chl->reply);
-        return chl->fastCheck() != ACCESS_ALLOWED; // when in doubt, block
+        return !chl->fastCheck().allowed(); // when in doubt, block
     }
-
-    // This does not happen, I hope, because we are called from CacheHit, which
-    // is called via a storeClientCopy() callback, and store should initialize
-    // the reply before calling that callback.
-    debugs(88, 3, "Missing reply!");
-    return false;
-}
-
-void
-clientReplyContext::purgeRequestFindObjectToPurge()
-{
-    /* Try to find a base entry */
-    http->flags.purging = true;
-    lookingforstore = 1;
-
-    // TODO: can we use purgeAllCached() here instead of doing the
-    // getPublicByRequestMethod() dance?
-    StoreEntry::getPublicByRequestMethod(this, http->request, Http::METHOD_GET);
 }
 
 // Purges all entries with a given url
@@ -838,30 +889,16 @@ clientReplyContext::purgeRequestFindObjectToPurge()
 void
 purgeEntriesByUrl(HttpRequest * req, const char *url)
 {
-#if USE_HTCP
-    bool get_or_head_sent = false;
-#endif
-
     for (HttpRequestMethod m(Http::METHOD_NONE); m != Http::METHOD_ENUM_END; ++m) {
         if (m.respMaybeCacheable()) {
-            if (StoreEntry *entry = storeGetPublic(url, m)) {
-                debugs(88, 5, "purging " << *entry << ' ' << m << ' ' << url);
+            const cache_key *key = storeKeyPublic(url, m);
+            debugs(88, 5, m << ' ' << url << ' ' << storeKeyText(key));
 #if USE_HTCP
-                neighborsHtcpClear(entry, url, req, m, HTCP_CLR_INVALIDATION);
-                if (m == Http::METHOD_GET || m == Http::METHOD_HEAD) {
-                    get_or_head_sent = true;
-                }
+            neighborsHtcpClear(nullptr, req, m, HTCP_CLR_INVALIDATION);
 #endif
-                entry->release();
-            }
+            Store::Root().evictIfFound(key);
         }
     }
-
-#if USE_HTCP
-    if (!get_or_head_sent) {
-        neighborsHtcpClear(NULL, url, req, HttpRequestMethod(Http::METHOD_GET), HTCP_CLR_INVALIDATION);
-    }
-#endif
 }
 
 void
@@ -872,75 +909,13 @@ clientReplyContext::purgeAllCached()
     purgeEntriesByUrl(http->request, url.c_str());
 }
 
-void
-clientReplyContext::created(StoreEntry *newEntry)
+LogTags *
+clientReplyContext::loggingTags() const
 {
-    if (lookingforstore == 1)
-        purgeFoundGet(newEntry);
-    else if (lookingforstore == 2)
-        purgeFoundHead(newEntry);
-    else if (lookingforstore == 3)
-        purgeDoPurgeGet(newEntry);
-    else if (lookingforstore == 4)
-        purgeDoPurgeHead(newEntry);
-    else if (lookingforstore == 5)
-        identifyFoundObject(newEntry);
-}
-
-void
-clientReplyContext::purgeFoundGet(StoreEntry *newEntry)
-{
-    if (newEntry->isNull()) {
-        lookingforstore = 2;
-        StoreEntry::getPublicByRequestMethod(this, http->request, Http::METHOD_HEAD);
-    } else
-        purgeFoundObject (newEntry);
-}
-
-void
-clientReplyContext::purgeFoundHead(StoreEntry *newEntry)
-{
-    if (newEntry->isNull())
-        purgeDoMissPurge();
-    else
-        purgeFoundObject (newEntry);
-}
-
-void
-clientReplyContext::purgeFoundObject(StoreEntry *entry)
-{
-    assert (entry && !entry->isNull());
-
-    if (EBIT_TEST(entry->flags, ENTRY_SPECIAL)) {
-        http->logType = LOG_TCP_DENIED;
-        ErrorState *err = clientBuildError(ERR_ACCESS_DENIED, Http::scForbidden, NULL,
-                                           http->getConn()->clientConnection->remote, http->request);
-        startError(err);
-        return; // XXX: leaking unused entry if some store does not keep it
-    }
-
-    StoreIOBuffer localTempBuffer;
-    /* Swap in the metadata */
-    http->storeEntry(entry);
-
-    http->storeEntry()->lock("clientReplyContext::purgeFoundObject");
-    http->storeEntry()->createMemObject(storeId(), http->log_uri,
-                                        http->request->method);
-
-    sc = storeClientListAdd(http->storeEntry(), this);
-
-    http->logType = LOG_TCP_HIT;
-
-    reqofs = 0;
-
-    localTempBuffer.offset = http->out.offset;
-
-    localTempBuffer.length = next()->readBuffer.length;
-
-    localTempBuffer.data = next()->readBuffer.data;
-
-    storeClientCopy(sc, http->storeEntry(),
-                    localTempBuffer, CacheHit, this);
+    // XXX: clientReplyContext code assumes that http cbdata is always valid.
+    // TODO: Either add cbdataReferenceValid(http) checks in all the relevant
+    // places, like this one, or remove cbdata protection of the http member.
+    return &http->logType;
 }
 
 void
@@ -950,8 +925,9 @@ clientReplyContext::purgeRequest()
            Config2.onoff.enable_purge);
 
     if (!Config2.onoff.enable_purge) {
-        http->logType = LOG_TCP_DENIED;
-        ErrorState *err = clientBuildError(ERR_ACCESS_DENIED, Http::scForbidden, NULL, http->getConn()->clientConnection->remote, http->request);
+        http->logType.update(LOG_TCP_DENIED);
+        ErrorState *err = clientBuildError(ERR_ACCESS_DENIED, Http::scForbidden, NULL,
+                                           http->getConn(), http->request, http->al);
         startError(err);
         return;
     }
@@ -959,99 +935,85 @@ clientReplyContext::purgeRequest()
     /* Release both IP cache */
     ipcacheInvalidate(http->request->url.host());
 
-    if (!http->flags.purging)
-        purgeRequestFindObjectToPurge();
-    else
-        purgeDoMissPurge();
+    // TODO: can we use purgeAllCached() here instead?
+    purgeDoPurge();
 }
 
 void
-clientReplyContext::purgeDoMissPurge()
-{
-    http->logType = LOG_TCP_MISS;
-    lookingforstore = 3;
-    StoreEntry::getPublicByRequestMethod(this,http->request, Http::METHOD_GET);
-}
-
-void
-clientReplyContext::purgeDoPurgeGet(StoreEntry *newEntry)
-{
-    assert (newEntry);
-    /* Move to new() when that is created */
-    purgeStatus = Http::scNotFound;
-
-    if (!newEntry->isNull()) {
-        /* Release the cached URI */
-        debugs(88, 4, "clientPurgeRequest: GET '" << newEntry->url() << "'" );
-#if USE_HTCP
-        neighborsHtcpClear(newEntry, NULL, http->request, HttpRequestMethod(Http::METHOD_GET), HTCP_CLR_PURGE);
-#endif
-        newEntry->release();
-        purgeStatus = Http::scOkay;
+clientReplyContext::purgeDoPurge()
+{
+    auto firstFound = false;
+    if (const auto entry = storeGetPublicByRequestMethod(http->request, Http::METHOD_GET)) {
+        // special entries are only METHOD_GET entries without variance
+        if (EBIT_TEST(entry->flags, ENTRY_SPECIAL)) {
+            http->logType.update(LOG_TCP_DENIED);
+            const auto err = clientBuildError(ERR_ACCESS_DENIED, Http::scForbidden, nullptr,
+                                              http->getConn(), http->request, http->al);
+            startError(err);
+            entry->abandon(__FUNCTION__);
+            return;
+        }
+        firstFound = true;
+        if (!purgeEntry(*entry, Http::METHOD_GET))
+            return;
     }
 
-    lookingforstore = 4;
-    StoreEntry::getPublicByRequestMethod(this, http->request, Http::METHOD_HEAD);
-}
+    detailStoreLookup(storeLookupString(firstFound));
 
-void
-clientReplyContext::purgeDoPurgeHead(StoreEntry *newEntry)
-{
-    if (newEntry && !newEntry->isNull()) {
-        debugs(88, 4, "HEAD " << newEntry->url());
-#if USE_HTCP
-        neighborsHtcpClear(newEntry, NULL, http->request, HttpRequestMethod(Http::METHOD_HEAD), HTCP_CLR_PURGE);
-#endif
-        newEntry->release();
-        purgeStatus = Http::scOkay;
+    if (const auto entry = storeGetPublicByRequestMethod(http->request, Http::METHOD_HEAD)) {
+        if (!purgeEntry(*entry, Http::METHOD_HEAD))
+            return;
     }
 
     /* And for Vary, release the base URI if none of the headers was included in the request */
-
-    if (http->request->vary_headers
-            && !strstr(http->request->vary_headers, "=")) {
+    if (!http->request->vary_headers.isEmpty()
+            && http->request->vary_headers.find('=') != SBuf::npos) {
         // XXX: performance regression, c_str() reallocates
         SBuf tmp(http->request->effectiveRequestUri());
-        StoreEntry *entry = storeGetPublic(tmp.c_str(), Http::METHOD_GET);
 
-        if (entry) {
-            debugs(88, 4, "Vary GET " << entry->url());
-#if USE_HTCP
-            neighborsHtcpClear(entry, NULL, http->request, HttpRequestMethod(Http::METHOD_GET), HTCP_CLR_PURGE);
-#endif
-            entry->release();
-            purgeStatus = Http::scOkay;
+        if (const auto entry = storeGetPublic(tmp.c_str(), Http::METHOD_GET)) {
+            if (!purgeEntry(*entry, Http::METHOD_GET, "Vary "))
+                return;
         }
 
-        entry = storeGetPublic(tmp.c_str(), Http::METHOD_HEAD);
-
-        if (entry) {
-            debugs(88, 4, "Vary HEAD " << entry->url());
-#if USE_HTCP
-            neighborsHtcpClear(entry, NULL, http->request, HttpRequestMethod(Http::METHOD_HEAD), HTCP_CLR_PURGE);
-#endif
-            entry->release();
-            purgeStatus = Http::scOkay;
+        if (const auto entry = storeGetPublic(tmp.c_str(), Http::METHOD_HEAD)) {
+            if (!purgeEntry(*entry, Http::METHOD_HEAD, "Vary "))
+                return;
         }
     }
 
+    if (purgeStatus == Http::scNone)
+        purgeStatus = Http::scNotFound;
+
     /*
      * Make a new entry to hold the reply to be written
      * to the client.
      */
-    /* FIXME: This doesn't need to go through the store. Simply
+    /* TODO: This doesn't need to go through the store. Simply
      * push down the client chain
      */
     createStoreEntry(http->request->method, RequestFlags());
 
     triggerInitialStoreRead();
 
-    HttpReply *rep = new HttpReply;
+    const HttpReplyPointer rep(new HttpReply);
     rep->setHeaders(purgeStatus, NULL, NULL, 0, 0, -1);
     http->storeEntry()->replaceHttpReply(rep);
     http->storeEntry()->complete();
 }
 
+bool
+clientReplyContext::purgeEntry(StoreEntry &entry, const Http::MethodType methodType, const char *descriptionPrefix)
+{
+    debugs(88, 4, descriptionPrefix << Http::MethodStr(methodType) << " '" << entry.url() << "'" );
+#if USE_HTCP
+    neighborsHtcpClear(&entry, http->request, HttpRequestMethod(methodType), HTCP_CLR_PURGE);
+#endif
+    entry.release(true);
+    purgeStatus = Http::scOkay;
+    return true;
+}
+
 void
 clientReplyContext::traceReply(clientStreamNode * node)
 {
@@ -1065,7 +1027,7 @@ clientReplyContext::traceReply(clientStreamNode * node)
                     localTempBuffer, SendMoreData, this);
     http->storeEntry()->releaseRequest();
     http->storeEntry()->buffer();
-    HttpReply *rep = new HttpReply;
+    const HttpReplyPointer rep(new HttpReply);
     rep->setHeaders(Http::scOkay, NULL, "text/plain", http->request->prefixLen(), 0, squid_curtime);
     http->storeEntry()->replaceHttpReply(rep);
     http->request->swapOut(http->storeEntry());
@@ -1132,22 +1094,30 @@ clientReplyContext::storeNotOKTransferDone() const
     MemObject *mem = http->storeEntry()->mem_obj;
     assert(mem != NULL);
     assert(http->request != NULL);
-    /* mem->reply was wrong because it uses the UPSTREAM header length!!! */
-    HttpReply const *curReply = mem->getReply();
 
+    /* mem->reply was wrong because it uses the UPSTREAM header length!!! */
     if (headers_sz == 0)
         /* haven't found end of headers yet */
         return 0;
 
+    // TODO: Use MemObject::expectedReplySize(method) after resolving XXX below.
+    const auto expectedBodySize = mem->baseReply().content_length;
+
+    // XXX: The code below talks about sending data, and checks stats about
+    // bytes written to the client connection, but this method must determine
+    // whether we are done _receiving_ data from Store. This code should work OK
+    // when expectedBodySize is unknown or matches written data, but it may
+    // malfunction when we are writing ranges while receiving a full response.
+
     /*
      * Figure out how much data we are supposed to send.
      * If we are sending a body and we don't have a content-length,
      * then we must wait for the object to become STORE_OK.
      */
-    if (curReply->content_length < 0)
+    if (expectedBodySize < 0)
         return 0;
 
-    int64_t expectedLength = curReply->content_length + http->out.headers_sz;
+    const uint64_t expectedLength = expectedBodySize + http->out.headers_sz;
 
     if (http->out.size < expectedLength)
         return 0;
@@ -1171,7 +1141,8 @@ clientHttpRequestStatus(int fd, ClientHttpRequest const *http)
 #if SIZEOF_INT64_T == 4
     if (http->out.size > 0x7FFF0000) {
         debugs(88, DBG_IMPORTANT, "WARNING: closing FD " << fd << " to prevent out.size counter overflow");
-        debugs(88, DBG_IMPORTANT, "\tclient " << http->getConn()->peer);
+        if (http->getConn())
+            debugs(88, DBG_IMPORTANT, "\tclient " << http->getConn()->peer);
         debugs(88, DBG_IMPORTANT, "\treceived " << http->out.size << " bytes");
         debugs(88, DBG_IMPORTANT, "\tURI " << http->log_uri);
         return 1;
@@ -1179,7 +1150,8 @@ clientHttpRequestStatus(int fd, ClientHttpRequest const *http)
 
     if (http->out.offset > 0x7FFF0000) {
         debugs(88, DBG_IMPORTANT, "WARNING: closing FD " << fd < " to prevent out.offset counter overflow");
-        debugs(88, DBG_IMPORTANT, "\tclient " << http->getConn()->peer);
+        if (http->getConn())
+            debugs(88, DBG_IMPORTANT, "\tclient " << http->getConn()->peer);
         debugs(88, DBG_IMPORTANT, "\treceived " << http->out.size << " bytes, offset " << http->out.offset);
         debugs(88, DBG_IMPORTANT, "\tURI " << http->log_uri);
         return 1;
@@ -1231,11 +1203,9 @@ clientReplyContext::replyStatus()
         debugs(88, 5, "clientReplyStatus: transfer is DONE: " << done << flags.complete);
         /* Ok we're finished, but how? */
 
-        const int64_t expectedBodySize =
-            http->storeEntry()->getReply()->bodySize(http->request->method);
-        if (!http->request->flags.proxyKeepalive && expectedBodySize < 0) {
-            debugs(88, 5, "clientReplyStatus: closing, content_length < 0");
-            return STREAM_FAILED;
+        if (EBIT_TEST(http->storeEntry()->flags, ENTRY_BAD_LENGTH)) {
+            debugs(88, 5, "clientReplyStatus: truncated response body");
+            return STREAM_UNPLANNED_COMPLETE;
         }
 
         if (!done) {
@@ -1243,6 +1213,9 @@ clientReplyContext::replyStatus()
             return STREAM_FAILED;
         }
 
+        // TODO: See also (and unify with) storeNotOKTransferDone() checks.
+        const int64_t expectedBodySize =
+            http->storeEntry()->mem().baseReply().bodySize(http->request->method);
         if (expectedBodySize >= 0 && !http->gotEnough()) {
             debugs(88, 5, "clientReplyStatus: client didn't get all it expected");
             return STREAM_UNPLANNED_COMPLETE;
@@ -1266,7 +1239,7 @@ clientReplyContext::replyStatus()
 /* Responses with no body will not have a content-type header,
  * which breaks the rep_mime_type acl, which
  * coincidentally, is the most common acl for reply access lists.
- * A better long term fix for this is to allow acl matchs on the various
+ * A better long term fix for this is to allow acl matches on the various
  * status codes, and then supply a default ruleset that puts these
  * codes before any user defines access entries. That way the user
  * can choose to block these responses where appropriate, but won't get
@@ -1317,16 +1290,24 @@ clientReplyContext::buildReplyHeader()
     hdr->delById(HDR_ETAG);
 #endif
 
-    if (is_hit)
+    if (is_hit || collapsedRevalidation == crSlave)
         hdr->delById(Http::HdrType::SET_COOKIE);
     // TODO: RFC 2965 : Must honour Cache-Control: no-cache="set-cookie2" and remove header.
 
     // if there is not configured a peer proxy with login=PASS or login=PASSTHRU option enabled
     // remove the Proxy-Authenticate header
-    if ( !request->peer_login || (strcmp(request->peer_login,"PASS") != 0 && strcmp(request->peer_login,"PASSTHRU") != 0))
-        reply->header.delById(Http::HdrType::PROXY_AUTHENTICATE);
+    if ( !request->peer_login || (strcmp(request->peer_login,"PASS") != 0 && strcmp(request->peer_login,"PASSTHRU") != 0)) {
+#if USE_ADAPTATION
+        // but allow adaptation services to authenticate clients
+        // via request satisfaction
+        if (!http->requestSatisfactionMode())
+#endif
+            reply->header.delById(Http::HdrType::PROXY_AUTHENTICATE);
+    }
 
     reply->header.removeHopByHopEntries();
+    // paranoid: ContentLengthInterpreter has cleaned non-generated replies
+    reply->removeIrrelevantContentLength();
 
     //    if (request->range)
     //      clientBuildRangeHeader(http, reply);
@@ -1348,19 +1329,19 @@ clientReplyContext::buildReplyHeader()
          */
         if (EBIT_TEST(http->storeEntry()->flags, ENTRY_SPECIAL)) {
             hdr->delById(Http::HdrType::DATE);
-            hdr->insertTime(Http::HdrType::DATE, squid_curtime);
+            hdr->putTime(Http::HdrType::DATE, squid_curtime);
         } else if (http->getConn() && http->getConn()->port->actAsOrigin) {
             // Swap the Date: header to current time if we are simulating an origin
             HttpHeaderEntry *h = hdr->findEntry(Http::HdrType::DATE);
             if (h)
                 hdr->putExt("X-Origin-Date", h->value.termedBuf());
             hdr->delById(Http::HdrType::DATE);
-            hdr->insertTime(Http::HdrType::DATE, squid_curtime);
+            hdr->putTime(Http::HdrType::DATE, squid_curtime);
             h = hdr->findEntry(Http::HdrType::EXPIRES);
             if (h && http->storeEntry()->expires >= 0) {
                 hdr->putExt("X-Origin-Expires", h->value.termedBuf());
                 hdr->delById(Http::HdrType::EXPIRES);
-                hdr->insertTime(Http::HdrType::EXPIRES, squid_curtime + http->storeEntry()->expires - http->storeEntry()->timestamp);
+                hdr->putTime(Http::HdrType::EXPIRES, squid_curtime + http->storeEntry()->expires - http->storeEntry()->timestamp);
             }
             if (http->storeEntry()->timestamp <= squid_curtime) {
                 // put X-Cache-Age: instead of Age:
@@ -1374,19 +1355,14 @@ clientReplyContext::buildReplyHeader()
             /* Signal old objects.  NB: rfc 2616 is not clear,
              * by implication, on whether we should do this to all
              * responses, or only cache hits.
-             * 14.46 states it ONLY applys for heuristically caclulated
+             * 14.46 states it ONLY applies for heuristically calculated
              * freshness values, 13.2.4 doesn't specify the same limitation.
              * We interpret RFC 2616 under the combination.
              */
             /* TODO: if maxage or s-maxage is present, don't do this */
 
-            if (squid_curtime - http->storeEntry()->timestamp >= 86400) {
-                char tbuf[512];
-                snprintf (tbuf, sizeof(tbuf), "%s %s %s",
-                          "113", ThisCache,
-                          "This cache hit is still fresh and more than 1 day old");
-                hdr->putStr(Http::HdrType::WARNING, tbuf);
-            }
+            if (squid_curtime - http->storeEntry()->timestamp >= 86400)
+                hdr->putWarning(113, "This cache hit is still fresh and more than 1 day old");
         }
     }
 
@@ -1399,9 +1375,9 @@ clientReplyContext::buildReplyHeader()
      */
     if ( !hdr->has(Http::HdrType::DATE) ) {
         if (!http->storeEntry())
-            hdr->insertTime(Http::HdrType::DATE, squid_curtime);
+            hdr->putTime(Http::HdrType::DATE, squid_curtime);
         else if (http->storeEntry()->timestamp > 0)
-            hdr->insertTime(Http::HdrType::DATE, http->storeEntry()->timestamp);
+            hdr->putTime(Http::HdrType::DATE, http->storeEntry()->timestamp);
         else {
             debugs(88,DBG_IMPORTANT,"BUG 3279: HTTP reply without Date:");
             /* dump something useful about the problem */
@@ -1466,30 +1442,28 @@ clientReplyContext::buildReplyHeader()
               reply->sline.status() == Http::scUnauthorized)
        ) {
         /* Add authentication header */
-        /*! \todo alter errorstate to be accel on|off aware. The 0 on the next line
+        /* TODO: alter errorstate to be accel on|off aware. The 0 on the next line
          * depends on authenticate behaviour: all schemes to date send no extra
          * data on 407/401 responses, and do not check the accel state on 401/407
          * responses
          */
-        authenticateFixHeader(reply, request->auth_user_request, request, 0, 1);
+        Auth::UserRequest::AddReplyAuthHeader(reply, request->auth_user_request, request, 0, 1);
     } else if (request->auth_user_request != NULL)
-        authenticateFixHeader(reply, request->auth_user_request, request, http->flags.accel, 0);
+        Auth::UserRequest::AddReplyAuthHeader(reply, request->auth_user_request, request, http->flags.accel, 0);
 #endif
 
-    /* Append X-Cache */
-    httpHeaderPutStrf(hdr, Http::HdrType::X_CACHE, "%s from %s",
-                      is_hit ? "HIT" : "MISS", getMyHostname());
-
-#if USE_CACHE_DIGESTS
-    /* Append X-Cache-Lookup: -- temporary hack, to be removed @?@ @?@ */
-    httpHeaderPutStrf(hdr, Http::HdrType::X_CACHE_LOOKUP, "%s from %s:%d",
-                      lookup_type ? lookup_type : "NONE",
-                      getMyHostname(), getMyPort());
-
-#endif
+    SBuf cacheStatus(uniqueHostname());
+    if (const auto hitOrFwd = http->logType.cacheStatusSource())
+        cacheStatus.append(hitOrFwd);
+    if (firstStoreLookup_) {
+        cacheStatus.append(";detail=");
+        cacheStatus.append(firstStoreLookup_);
+    }
+    // TODO: Remove c_str() after converting HttpHeaderEntry::value to SBuf
+    hdr->putStr(Http::HdrType::CACHE_STATUS, cacheStatus.c_str());
 
     const bool maySendChunkedReply = !request->multipartRangeRequest() &&
-                                     reply->sline.protocol == AnyP::PROTO_HTTP && // response is HTTP
+                                     reply->sline.version.protocol == AnyP::PROTO_HTTP && // response is HTTP
                                      (request->http_ver >= Http::ProtocolVersion(1,1));
 
     /* Check whether we should send keep-alive */
@@ -1529,27 +1503,14 @@ clientReplyContext::buildReplyHeader()
     }
 
     // Decide if we send chunked reply
-    if (maySendChunkedReply &&
-            request->flags.proxyKeepalive &&
-            reply->bodySize(request->method) < 0) {
+    if (maySendChunkedReply && reply->bodySize(request->method) < 0) {
         debugs(88, 3, "clientBuildReplyHeader: chunked reply");
         request->flags.chunkedReply = true;
         hdr->putStr(Http::HdrType::TRANSFER_ENCODING, "chunked");
     }
 
-    /* Append VIA */
-    if (Config.onoff.via) {
-        LOCAL_ARRAY(char, bbuf, MAX_URL + 32);
-        String strVia;
-        hdr->getList(Http::HdrType::VIA, &strVia);
-        snprintf(bbuf, MAX_URL + 32, "%d.%d %s",
-                 reply->sline.version.major,
-                 reply->sline.version.minor,
-                 ThisCache);
-        strListAdd(&strVia, bbuf, ',');
-        hdr->delById(Http::HdrType::VIA);
-        hdr->putStr(Http::HdrType::VIA, strVia.termedBuf());
-    }
+    hdr->addVia(reply->sline.version);
+
     /* Signal keep-alive or close explicitly */
     hdr->putStr(Http::HdrType::CONNECTION, request->flags.proxyKeepalive ? "keep-alive" : "close");
 
@@ -1573,7 +1534,7 @@ clientReplyContext::buildReplyHeader()
         /* TODO: else case: drop any controls intended specifically for our surrogate ID */
     }
 
-    httpHdrMangleList(hdr, request, ROR_REPLY);
+    httpHdrMangleList(hdr, request, http->al, ROR_REPLY);
 }
 
 void
@@ -1581,10 +1542,12 @@ clientReplyContext::cloneReply()
 {
     assert(reply == NULL);
 
-    reply = http->storeEntry()->getReply()->clone();
+    reply = http->storeEntry()->mem().freshestReply().clone();
     HTTPMSGLOCK(reply);
 
-    if (reply->sline.protocol == AnyP::PROTO_HTTP) {
+    http->al->reply = reply;
+
+    if (reply->sline.version.protocol == AnyP::PROTO_HTTP) {
         /* RFC 2616 requires us to advertise our version (but only on real HTTP traffic) */
         reply->sline.version = Http::ProtocolVersion();
     }
@@ -1615,11 +1578,14 @@ clientReplyContext::identifyStoreObject()
 {
     HttpRequest *r = http->request;
 
-    if (r->flags.cachable || r->flags.internal) {
-        lookingforstore = 5;
-        StoreEntry::getPublicByRequest (this, r);
+    // client sent CC:no-cache or some other condition has been
+    // encountered which prevents delivering a public/cached object.
+    if (!r->flags.noCache || r->flags.internal) {
+        const auto e = storeGetPublicByRequest(r);
+        identifyFoundObject(e, storeLookupString(bool(e)));
     } else {
-        identifyFoundObject (NullStoreEntry::getInstance());
+        // "external" no-cache requests skip Store lookups
+        identifyFoundObject(nullptr, "no-cache");
     }
 }
 
@@ -1628,19 +1594,13 @@ clientReplyContext::identifyStoreObject()
  * to see if we can determine the final status of the request.
  */
 void
-clientReplyContext::identifyFoundObject(StoreEntry *newEntry)
+clientReplyContext::identifyFoundObject(StoreEntry *newEntry, const char *detail)
 {
-    StoreEntry *e = newEntry;
-    HttpRequest *r = http->request;
+    detailStoreLookup(detail);
 
-    /** \li If the entry received isNull() then we ignore it. */
-    if (e->isNull()) {
-        http->storeEntry(NULL);
-    } else {
-        http->storeEntry(e);
-    }
-
-    e = http->storeEntry();
+    HttpRequest *r = http->request;
+    http->storeEntry(newEntry);
+    const auto e = http->storeEntry();
 
     /* Release IP-cache entries on reload */
     /** \li If the request has no-cache flag set or some no_cache HACK in operation we
@@ -1649,14 +1609,10 @@ clientReplyContext::identifyFoundObject(StoreEntry *newEntry)
     if (r->flags.noCache || r->flags.noCacheHack())
         ipcacheInvalidateNegative(r->url.host());
 
-#if USE_CACHE_DIGESTS
-    lookup_type = http->storeEntry() ? "HIT" : "MISS";
-#endif
-
-    if (NULL == http->storeEntry()) {
+    if (!e) {
         /** \li If no StoreEntry object is current assume this object isn't in the cache set MISS*/
         debugs(85, 3, "StoreEntry is NULL -  MISS");
-        http->logType = LOG_TCP_MISS;
+        http->logType.update(LOG_TCP_MISS);
         doGetMoreData();
         return;
     }
@@ -1664,7 +1620,7 @@ clientReplyContext::identifyFoundObject(StoreEntry *newEntry)
     if (Config.onoff.offline) {
         /** \li If we are running in offline mode set to HIT */
         debugs(85, 3, "offline HIT " << *e);
-        http->logType = LOG_TCP_HIT;
+        http->logType.update(LOG_TCP_HIT);
         doGetMoreData();
         return;
     }
@@ -1673,7 +1629,7 @@ clientReplyContext::identifyFoundObject(StoreEntry *newEntry)
         /** \li If redirection status is True force this to be a MISS */
         debugs(85, 3, "REDIRECT status forced StoreEntry to NULL (no body on 3XX responses) " << *e);
         forgetHit();
-        http->logType = LOG_TCP_REDIRECT;
+        http->logType.update(LOG_TCP_REDIRECT);
         doGetMoreData();
         return;
     }
@@ -1681,7 +1637,7 @@ clientReplyContext::identifyFoundObject(StoreEntry *newEntry)
     if (!e->validToSend()) {
         debugs(85, 3, "!storeEntryValidToSend MISS " << *e);
         forgetHit();
-        http->logType = LOG_TCP_MISS;
+        http->logType.update(LOG_TCP_MISS);
         doGetMoreData();
         return;
     }
@@ -1689,7 +1645,7 @@ clientReplyContext::identifyFoundObject(StoreEntry *newEntry)
     if (EBIT_TEST(e->flags, ENTRY_SPECIAL)) {
         /* \li Special entries are always hits, no matter what the client says */
         debugs(85, 3, "ENTRY_SPECIAL HIT " << *e);
-        http->logType = LOG_TCP_HIT;
+        http->logType.update(LOG_TCP_HIT);
         doGetMoreData();
         return;
     }
@@ -1697,16 +1653,36 @@ clientReplyContext::identifyFoundObject(StoreEntry *newEntry)
     if (r->flags.noCache) {
         debugs(85, 3, "no-cache REFRESH MISS " << *e);
         forgetHit();
-        http->logType = LOG_TCP_CLIENT_REFRESH_MISS;
+        http->logType.update(LOG_TCP_CLIENT_REFRESH_MISS);
+        doGetMoreData();
+        return;
+    }
+
+    if (e->hittingRequiresCollapsing() && !startCollapsingOn(*e, false)) {
+        debugs(85, 3, "prohibited CF MISS " << *e);
+        forgetHit();
+        http->logType.update(LOG_TCP_MISS);
         doGetMoreData();
         return;
     }
 
     debugs(85, 3, "default HIT " << *e);
-    http->logType = LOG_TCP_HIT;
+    http->logType.update(LOG_TCP_HIT);
     doGetMoreData();
 }
 
+/// remembers the very first Store lookup classification, ignoring the rest
+void
+clientReplyContext::detailStoreLookup(const char *detail)
+{
+    if (!firstStoreLookup_) {
+        debugs(85, 7, detail);
+        firstStoreLookup_ = detail;
+    } else {
+        debugs(85, 7, "ignores " << detail << " after " << firstStoreLookup_);
+    }
+}
+
 /**
  * Request more data from the store for the client Stream
  * This is *the* entry point to this module.
@@ -1759,7 +1735,7 @@ clientGetMoreData(clientStreamNode * aNode, ClientHttpRequest * http)
         }
 
         /* continue forwarding, not finished yet. */
-        http->logType = LOG_TCP_MISS;
+        http->logType.update(LOG_TCP_MISS);
 
         context->doGetMoreData();
     } else
@@ -1776,22 +1752,7 @@ clientReplyContext::doGetMoreData()
 
         http->storeEntry()->lock("clientReplyContext::doGetMoreData");
 
-        MemObject *mem_obj = http->storeEntry()->makeMemObject();
-        if (!mem_obj->hasUris()) {
-            /*
-             * This if-block exists because we don't want to clobber
-             * a preexiting mem_obj->method value if the mem_obj
-             * already exists.  For example, when a HEAD request
-             * is a cache hit for a GET response, we want to keep
-             * the method as GET.
-             */
-            mem_obj->setUris(storeId(), http->log_uri, http->request->method);
-            /**
-             * Here we can see if the object was
-             * created using URL or alternative StoreID from helper.
-             */
-            debugs(88, 3, "storeId: " << http->storeEntry()->mem_obj->storeId());
-        }
+        http->storeEntry()->ensureMemObject(storeId(), http->log_uri, http->request->method);
 
         sc = storeClientListAdd(http->storeEntry(), this);
 #if USE_DELAY_POOLS
@@ -1804,12 +1765,14 @@ clientReplyContext::doGetMoreData()
         assert(http->out.size == 0);
         assert(http->out.offset == 0);
 
-        if (Ip::Qos::TheConfig.isHitTosActive()) {
-            Ip::Qos::doTosLocalHit(http->getConn()->clientConnection);
-        }
+        if (ConnStateData *conn = http->getConn()) {
+            if (Ip::Qos::TheConfig.isHitTosActive()) {
+                Ip::Qos::doTosLocalHit(conn->clientConnection);
+            }
 
-        if (Ip::Qos::TheConfig.isHitNfmarkActive()) {
-            Ip::Qos::doNfmarkLocalHit(http->getConn()->clientConnection);
+            if (Ip::Qos::TheConfig.isHitNfmarkActive()) {
+                Ip::Qos::doNfmarkLocalHit(conn->clientConnection);
+            }
         }
 
         localTempBuffer.offset = reqofs;
@@ -1844,7 +1807,7 @@ clientReplyContext::SendMoreData(void *data, StoreIOBuffer result)
 void
 clientReplyContext::makeThisHead()
 {
-    /* At least, I think thats what this does */
+    /* At least, I think that's what this does */
     dlinkDelete(&http->active, &ClientActiveRequests);
     dlinkAdd(http, &http->active, &ClientActiveRequests);
 }
@@ -1866,7 +1829,7 @@ clientReplyContext::sendStreamError(StoreIOBuffer const &result)
      * We call into the stream, because we don't know that there is a
      * client socket!
      */
-    debugs(88, 5, "clientReplyContext::sendStreamError: A stream error has occured, marking as complete and sending no data.");
+    debugs(88, 5, "A stream error has occurred, marking as complete and sending no data.");
     StoreIOBuffer localTempBuffer;
     flags.complete = 1;
     http->request->flags.streamError = true;
@@ -1906,12 +1869,9 @@ clientReplyContext::next() const
 void
 clientReplyContext::sendBodyTooLargeError()
 {
-    Ip::Address tmp_noaddr;
-    tmp_noaddr.setNoAddr(); // TODO: make a global const
-    http->logType = LOG_TCP_DENIED_REPLY;
+    http->logType.update(LOG_TCP_DENIED_REPLY);
     ErrorState *err = clientBuildError(ERR_TOO_BIG, Http::scForbidden, NULL,
-                                       http->getConn() != NULL ? http->getConn()->clientConnection->remote : tmp_noaddr,
-                                       http->request);
+                                       http->getConn(), http->request, http->al);
     removeClientStoreReference(&(sc), http);
     HTTPMSGUNLOCK(reply);
     startError(err);
@@ -1922,10 +1882,10 @@ clientReplyContext::sendBodyTooLargeError()
 void
 clientReplyContext::sendPreconditionFailedError()
 {
-    http->logType = LOG_TCP_HIT;
+    http->logType.update(LOG_TCP_HIT);
     ErrorState *const err =
         clientBuildError(ERR_PRECONDITION_FAILED, Http::scPreconditionFailed,
-                         NULL, http->getConn()->clientConnection->remote, http->request);
+                         nullptr, http->getConn(), http->request, http->al);
     removeClientStoreReference(&sc, http);
     HTTPMSGUNLOCK(reply);
     startError(err);
@@ -1937,8 +1897,13 @@ clientReplyContext::sendNotModified()
 {
     StoreEntry *e = http->storeEntry();
     const time_t timestamp = e->timestamp;
-    HttpReply *const temprep = e->getReply()->make304();
-    http->logType = LOG_TCP_IMS_HIT;
+    const auto temprep = e->mem().freshestReply().make304();
+    // log as TCP_INM_HIT if code 304 generated for
+    // If-None-Match request
+    if (!http->request->flags.ims)
+        http->logType.update(LOG_TCP_INM_HIT);
+    else
+        http->logType.update(LOG_TCP_IMS_HIT);
     removeClientStoreReference(&sc, http);
     createStoreEntry(http->request->method, RequestFlags());
     e = http->storeEntry();
@@ -2006,34 +1971,31 @@ clientReplyContext::processReplyAccess ()
 }
 
 void
-clientReplyContext::ProcessReplyAccessResult(allow_t rv, void *voidMe)
+clientReplyContext::ProcessReplyAccessResult(Acl::Answer rv, void *voidMe)
 {
     clientReplyContext *me = static_cast<clientReplyContext *>(voidMe);
     me->processReplyAccessResult(rv);
 }
 
 void
-clientReplyContext::processReplyAccessResult(const allow_t &accessAllowed)
+clientReplyContext::processReplyAccessResult(const Acl::Answer &accessAllowed)
 {
     debugs(88, 2, "The reply for " << http->request->method
            << ' ' << http->uri << " is " << accessAllowed << ", because it matched "
            << (AclMatchedName ? AclMatchedName : "NO ACL's"));
 
-    if (accessAllowed != ACCESS_ALLOWED) {
+    if (!accessAllowed.allowed()) {
         ErrorState *err;
         err_type page_id;
         page_id = aclGetDenyInfoPage(&Config.denyInfoList, AclMatchedName, 1);
 
-        http->logType = LOG_TCP_DENIED_REPLY;
+        http->logType.update(LOG_TCP_DENIED_REPLY);
 
         if (page_id == ERR_NONE)
             page_id = ERR_ACCESS_DENIED;
 
-        Ip::Address tmp_noaddr;
-        tmp_noaddr.setNoAddr();
         err = clientBuildError(page_id, Http::scForbidden, NULL,
-                               http->getConn() != NULL ? http->getConn()->clientConnection->remote : tmp_noaddr,
-                               http->request);
+                               http->getConn(), http->request, http->al);
 
         removeClientStoreReference(&sc, http);
 
@@ -2119,21 +2081,28 @@ clientReplyContext::sendMoreData (StoreIOBuffer result)
 
     StoreEntry *entry = http->storeEntry();
 
-    ConnStateData * conn = http->getConn();
+    if (ConnStateData * conn = http->getConn()) {
+        if (!conn->isOpen()) {
+            debugs(33,3, "not sending more data to closing connection " << conn->clientConnection);
+            return;
+        }
+        if (conn->pinning.zeroReply) {
+            debugs(33,3, "not sending more data after a pinned zero reply " << conn->clientConnection);
+            return;
+        }
 
-    // too late, our conn is closing
-    // TODO: should we also quit?
-    if (conn == NULL) {
-        debugs(33,3, "not sending more data to a closed connection" );
-        return;
-    }
-    if (!conn->isOpen()) {
-        debugs(33,3, "not sending more data to closing connection " << conn->clientConnection);
-        return;
-    }
-    if (conn->pinning.zeroReply) {
-        debugs(33,3, "not sending more data after a pinned zero reply " << conn->clientConnection);
-        return;
+        if (reqofs==0 && !http->logType.isTcpHit()) {
+            if (Ip::Qos::TheConfig.isHitTosActive()) {
+                Ip::Qos::doTosLocalMiss(conn->clientConnection, http->request->hier.code);
+            }
+            if (Ip::Qos::TheConfig.isHitNfmarkActive()) {
+                Ip::Qos::doNfmarkLocalMiss(conn->clientConnection, http->request->hier.code);
+            }
+        }
+
+        debugs(88, 5, conn->clientConnection <<
+               " '" << entry->url() << "'" <<
+               " out.offset=" << http->out.offset);
     }
 
     char *buf = next()->readBuffer.data;
@@ -2144,15 +2113,6 @@ clientReplyContext::sendMoreData (StoreIOBuffer result)
         memcpy(buf, result.data, result.length);
     }
 
-    if (reqofs==0 && !http->logType.isTcpHit() && Comm::IsConnOpen(conn->clientConnection)) {
-        if (Ip::Qos::TheConfig.isHitTosActive()) {
-            Ip::Qos::doTosLocalMiss(conn->clientConnection, http->request->hier.code);
-        }
-        if (Ip::Qos::TheConfig.isHitNfmarkActive()) {
-            Ip::Qos::doNfmarkLocalMiss(conn->clientConnection, http->request->hier.code);
-        }
-    }
-
     /* We've got the final data to start pushing... */
     flags.storelogiccomplete = 1;
 
@@ -2171,10 +2131,6 @@ clientReplyContext::sendMoreData (StoreIOBuffer result)
     debugs(88, 5, "clientReplyContext::sendMoreData: " << http->uri << ", " <<
            reqofs << " bytes (" << result.length <<
            " new bytes)");
-    debugs(88, 5, "clientReplyContext::sendMoreData:"
-           << conn->clientConnection <<
-           " '" << entry->url() << "'" <<
-           " out.offset=" << http->out.offset);
 
     /* update size of the request */
     reqsize = reqofs;
@@ -2191,23 +2147,22 @@ clientReplyContext::sendMoreData (StoreIOBuffer result)
 
     cloneReply();
 
-    /* handle headers */
-
-    if (Config.onoff.log_mime_hdrs) {
-        size_t k;
-
-        if ((k = headersEnd(buf, reqofs))) {
-            safe_free(http->al->headers.reply);
-            http->al->headers.reply = (char *)xcalloc(k + 1, 1);
-            xstrncpy(http->al->headers.reply, buf, k);
-        }
-    }
+#if USE_DELAY_POOLS
+    if (sc)
+        sc->setDelayId(DelayId::DelayClient(http,reply));
+#endif
 
     holdingBuffer = result;
     processReplyAccess();
     return;
 }
 
+void
+clientReplyContext::fillChecklist(ACLFilledChecklist &checklist) const
+{
+    clientAclChecklistFill(checklist, http);
+}
+
 /* Using this breaks the client layering just a little!
  */
 void
@@ -2220,20 +2175,25 @@ clientReplyContext::createStoreEntry(const HttpRequestMethod& m, RequestFlags re
      */
 
     if (http->request == NULL) {
-        http->request = new HttpRequest(m, AnyP::PROTO_NONE, null_string);
+        const MasterXaction::Pointer mx = new MasterXaction(XactionInitiator::initClient);
+        // XXX: These fake URI parameters shadow the real (or error:...) URI.
+        // TODO: Either always set the request earlier and assert here OR use
+        // http->uri (converted to Anyp::Uri) to create this catch-all request.
+        const_cast<HttpRequest *&>(http->request) =  new HttpRequest(m, AnyP::PROTO_NONE, "http", null_string, mx);
         HTTPMSGLOCK(http->request);
     }
 
     StoreEntry *e = storeCreateEntry(storeId(), http->log_uri, reqFlags, m);
 
-    // Make entry collapsable ASAP, to increase collapsing chances for others,
+    // Make entry collapsible ASAP, to increase collapsing chances for others,
     // TODO: every must-revalidate and similar request MUST reach the origin,
     // but do we have to prohibit others from collapsing on that request?
-    if (Config.onoff.collapsed_forwarding && reqFlags.cachable &&
+    if (reqFlags.cachable &&
             !reqFlags.needValidation &&
-            (m == Http::METHOD_GET || m == Http::METHOD_HEAD)) {
+            (m == Http::METHOD_GET || m == Http::METHOD_HEAD) &&
+            mayInitiateCollapsing()) {
         // make the entry available for future requests now
-        Store::Root().allowCollapsing(e, reqFlags, m);
+        (void)Store::Root().allowCollapsing(e, reqFlags, m);
     }
 
     sc = storeClientListAdd(e, this);
@@ -2257,7 +2217,7 @@ clientReplyContext::createStoreEntry(const HttpRequestMethod& m, RequestFlags re
     /* So, we mark the store logic as complete */
     flags.storelogiccomplete = 1;
 
-    /* and get the caller to request a read, from whereever they are */
+    /* and get the caller to request a read, from wherever they are */
     /* NOTE: after ANY data flows down the pipe, even one step,
      * this function CAN NOT be used to manage errors
      */
@@ -2266,10 +2226,10 @@ clientReplyContext::createStoreEntry(const HttpRequestMethod& m, RequestFlags re
 
 ErrorState *
 clientBuildError(err_type page_id, Http::StatusCode status, char const *url,
-                 Ip::Address &src_addr, HttpRequest * request)
+                 const ConnStateData *conn, HttpRequest *request, const AccessLogEntry::Pointer &al)
 {
-    ErrorState *err = new ErrorState(page_id, status, request);
-    err->src_addr = src_addr;
+    const auto err = new ErrorState(page_id, status, request, al);
+    err->src_addr = conn && conn->clientConnection ? conn->clientConnection->remote : Ip::Address::NoAddr();
 
     if (url)
         err->url = xstrdup(url);