]> 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 a367b5b67481db3d9ae68651cc6aa64d90e72410..1512772704ef526b913050eaf457db08016872d0 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 1996-2017 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.
@@ -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,20 +74,17 @@ 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),
+    old_lastmod(-1),
     deleting(false),
     collapsedRevalidation(crNone)
 {
@@ -103,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
@@ -111,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);
@@ -204,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 */
@@ -223,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;
 }
@@ -279,7 +281,7 @@ clientReplyContext::processExpired()
         return;
     }
 
-    http->logType = LOG_TCP_REFRESH;
+    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
@@ -289,18 +291,28 @@ clientReplyContext::processExpired()
     /* Prepare to make a new temporary request */
     saveState();
 
+    // TODO: Consider also allowing regular (non-collapsed) revalidation hits.
     // TODO: support collapsed revalidation for Vary-controlled entries
-    const bool collapsingAllowed = Config.onoff.collapsed_forwarding &&
-                                   !Store::Root().smpAware() &&
-                                   http->request->vary_headers.isEmpty();
+    bool collapsingAllowed = Config.onoff.collapsed_forwarding &&
+                             !Store::Controller::SmpAware() &&
+                             http->request->vary_headers.isEmpty();
 
     StoreEntry *entry = nullptr;
     if (collapsingAllowed) {
-        if ((entry = storeGetPublicByRequest(http->request, ksRevalidation)))
-            entry->lock("clientReplyContext::processExpired#alreadyRevalidating");
+        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 {
@@ -308,10 +320,9 @@ clientReplyContext::processExpired()
                                  http->log_uri, http->request->flags, http->request->method);
         /* NOTE, don't call StoreEntry->lock(), storeCreateEntry() does it */
 
-        if (collapsingAllowed) {
+        if (collapsingAllowed && mayInitiateCollapsing() &&
+                Store::Root().allowCollapsing(entry, http->request->flags, http->request->method)) {
             debugs(88, 5, "allow other revalidation requests to collapse on " << *entry);
-            Store::Root().allowCollapsing(entry, http->request->flags,
-                                          http->request->method);
             collapsedRevalidation = crInitiator;
         } else {
             collapsedRevalidation = crNone;
@@ -362,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;
@@ -419,7 +434,7 @@ clientReplyContext::handleIMSReply(StoreIOBuffer result)
         debugs(88, 3, "CF slave hit private non-shareable " << *http->storeEntry() << ". MISS");
         // restore context to meet processMiss() expectations
         restoreState();
-        http->logType = LOG_TCP_MISS;
+        http->logType.update(LOG_TCP_MISS);
         processMiss();
         return;
     }
@@ -427,75 +442,73 @@ clientReplyContext::handleIMSReply(StoreIOBuffer result)
     /* 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, "request to origin aborted '" << http->storeEntry()->url() << "', sending old entry to client");
-        http->logType = LOG_TCP_REFRESH_FAIL_OLD;
+        http->logType.update(LOG_TCP_REFRESH_FAIL_OLD);
         sendClientOldEntry();
+        return;
     }
 
-    const HttpReply *old_rep = 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
+        // 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->ims, http->request->imslen)) {
             // forward the 304 from origin
-            debugs(88, 3, "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, "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) {
-        const HttpReply *new_rep = http->storeEntry()->getReply();
+    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_rep)) {
+        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 (" <<
-                   old_rep->sline.status() << ") to client");
+            debugs(88, 3, "origin replied " << status << " but with an older date header, sending old entry (" << oldStatus << ") to client");
             sendClientOldEntry();
-        } else {
-            http->logType = LOG_TCP_REFRESH_MODIFIED;
-            debugs(88, 3, "origin replied " << status <<
-                   ", replacing existing entry and forwarding to client");
-
-            if (collapsedRevalidation)
-                http->storeEntry()->clearPublicKeyScope();
-
-            sendClientUpstreamResponse();
+            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, "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, "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;
@@ -539,18 +552,18 @@ 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 unsharable!
+    // 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, "unsharable " << *e << ". MISS");
-        http->logType = LOG_TCP_MISS;
+        debugs(88, 3, "unshareable " << *e << ". MISS");
+        http->logType.update(LOG_TCP_MISS);
         processMiss();
         return;
     }
@@ -561,7 +574,7 @@ clientReplyContext::cacheHit(StoreIOBuffer result)
          * object
          */
         /* treat as a miss */
-        http->logType = LOG_TCP_MISS;
+        http->logType.update(LOG_TCP_MISS);
         processMiss();
         return;
     }
@@ -577,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;
     }
@@ -609,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;
     }
@@ -624,12 +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)) {
@@ -653,7 +666,7 @@ clientReplyContext::cacheHit(StoreIOBuffer result)
              * 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");
@@ -661,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
@@ -676,7 +689,7 @@ 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;
@@ -693,13 +706,13 @@ clientReplyContext::cacheHit(StoreIOBuffer result)
 
 #if USE_DELAY_POOLS
     if (e->store_status != STORE_OK)
-        http->logType = LOG_TCP_MISS;
+        http->logType.update(LOG_TCP_MISS);
     else
 #endif
         if (e->mem_status == IN_MEMORY)
-            http->logType = LOG_TCP_MEM_HIT;
+            http->logType.update(LOG_TCP_MEM_HIT);
         else if (Config.onoff.offline)
-            http->logType = LOG_TCP_OFFLINE_HIT;
+            http->logType.update(LOG_TCP_OFFLINE_HIT);
 
     sendMoreData(result);
 }
@@ -749,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();
@@ -760,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);
@@ -771,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);
     }
 }
@@ -789,7 +802,7 @@ 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);
 }
@@ -800,9 +813,10 @@ clientReplyContext::processConditional(StoreIOBuffer &result)
 {
     StoreEntry *const e = http->storeEntry();
 
-    if (e->getReply()->sline.status() != Http::scOkay) {
-        debugs(88, 4, "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 true;
     }
@@ -857,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
@@ -892,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
@@ -926,75 +909,13 @@ clientReplyContext::purgeAllCached()
     purgeEntriesByUrl(http->request, url.c_str());
 }
 
-void
-clientReplyContext::created(StoreEntry *newEntry)
-{
-    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)
+LogTags *
+clientReplyContext::loggingTags() const
 {
-    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
@@ -1004,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;
     }
@@ -1013,51 +935,34 @@ clientReplyContext::purgeRequest()
     /* Release both IP cache */
     ipcacheInvalidate(http->request->url.host());
 
-    if (!http->flags.purging)
-        purgeRequestFindObjectToPurge();
-    else
-        purgeDoMissPurge();
-}
-
-void
-clientReplyContext::purgeDoMissPurge()
-{
-    http->logType = LOG_TCP_MISS;
-    lookingforstore = 3;
-    StoreEntry::getPublicByRequestMethod(this,http->request, Http::METHOD_GET);
+    // TODO: can we use purgeAllCached() here instead?
+    purgeDoPurge();
 }
 
 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 */
@@ -1065,46 +970,50 @@ clientReplyContext::purgeDoPurgeHead(StoreEntry *newEntry)
             && 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)
 {
@@ -1118,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());
@@ -1191,17 +1100,24 @@ clientReplyContext::storeNotOKTransferDone() const
         /* haven't found end of headers yet */
         return 0;
 
-    const HttpReplyPointer curReply(mem->getReply());
+    // 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;
 
-    uint64_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;
@@ -1225,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;
@@ -1233,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;
@@ -1295,8 +1213,9 @@ clientReplyContext::replyStatus()
             return STREAM_FAILED;
         }
 
+        // TODO: See also (and unify with) storeNotOKTransferDone() checks.
         const int64_t expectedBodySize =
-            http->storeEntry()->getReply()->bodySize(http->request->method);
+            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;
@@ -1320,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
@@ -1387,6 +1306,8 @@ clientReplyContext::buildReplyHeader()
     }
 
     reply->header.removeHopByHopEntries();
+    // paranoid: ContentLengthInterpreter has cleaned non-generated replies
+    reply->removeIrrelevantContentLength();
 
     //    if (request->range)
     //      clientBuildRangeHeader(http, reply);
@@ -1434,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");
         }
     }
 
@@ -1526,7 +1442,7 @@ 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
@@ -1536,20 +1452,18 @@ clientReplyContext::buildReplyHeader()
         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 */
@@ -1628,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();
     }
@@ -1665,10 +1581,11 @@ clientReplyContext::identifyStoreObject()
     // 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) {
-        lookingforstore = 5;
-        StoreEntry::getPublicByRequest (this, r);
+        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");
     }
 }
 
@@ -1677,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;
-
-    /** \li If the entry received isNull() then we ignore it. */
-    if (e->isNull()) {
-        http->storeEntry(NULL);
-    } else {
-        http->storeEntry(e);
-    }
+    detailStoreLookup(detail);
 
-    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
@@ -1698,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;
     }
@@ -1713,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;
     }
@@ -1722,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;
     }
@@ -1730,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;
     }
@@ -1738,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;
     }
@@ -1746,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.
@@ -1808,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
@@ -1825,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
@@ -1853,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;
@@ -1893,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);
 }
@@ -1915,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;
@@ -1955,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);
@@ -1971,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);
@@ -1986,13 +1897,13 @@ clientReplyContext::sendNotModified()
 {
     StoreEntry *e = http->storeEntry();
     const time_t timestamp = e->timestamp;
-    HttpReply *const temprep = e->getReply()->make304();
+    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 = LOG_TCP_INM_HIT;
+        http->logType.update(LOG_TCP_INM_HIT);
     else
-        http->logType = LOG_TCP_IMS_HIT;
+        http->logType.update(LOG_TCP_IMS_HIT);
     removeClientStoreReference(&sc, http);
     createStoreEntry(http->request->method, RequestFlags());
     e = http->storeEntry();
@@ -2060,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);
 
@@ -2244,23 +2152,17 @@ clientReplyContext::sendMoreData (StoreIOBuffer result)
         sc->setDelayId(DelayId::DelayClient(http,reply));
 #endif
 
-    /* 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);
-        }
-    }
-
     holdingBuffer = result;
     processReplyAccess();
     return;
 }
 
+void
+clientReplyContext::fillChecklist(ACLFilledChecklist &checklist) const
+{
+    clientAclChecklistFill(checklist, http);
+}
+
 /* Using this breaks the client layering just a little!
  */
 void
@@ -2273,20 +2175,25 @@ clientReplyContext::createStoreEntry(const HttpRequestMethod& m, RequestFlags re
      */
 
     if (http->request == NULL) {
-        http->request = new HttpRequest(m, AnyP::PROTO_NONE, "http", 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);
@@ -2310,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
      */
@@ -2319,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);