]> git.ipfire.org Git - thirdparty/squid.git/blobdiff - src/errorpage.cc
Source Format Enforcement (#1234)
[thirdparty/squid.git] / src / errorpage.cc
index e219f7b00b909b61263adcd53d2fbfbf19a3f640..4d257bc8c6ce2971489b23164c5ae9d50468ed1f 100644 (file)
@@ -1,43 +1,25 @@
 /*
- * DEBUG: section 04    Error Generation
- * AUTHOR: Duane Wessels
- *
- * SQUID Web Proxy Cache          http://www.squid-cache.org/
- * ----------------------------------------------------------
- *
- *  Squid is the result of efforts by numerous individuals from
- *  the Internet community; see the CONTRIBUTORS file for full
- *  details.   Many organizations have provided support for Squid's
- *  development; see the SPONSORS file for full details.  Squid is
- *  Copyrighted (C) 2001 by the Regents of the University of
- *  California; see the COPYRIGHT file for full details.  Squid
- *  incorporates software developed and/or copyrighted by other
- *  sources; see the CREDITS file for full details.
- *
- *  This program is free software; you can redistribute it and/or modify
- *  it under the terms of the GNU General Public License as published by
- *  the Free Software Foundation; either version 2 of the License, or
- *  (at your option) any later version.
- *
- *  This program is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *  GNU General Public License for more details.
- *
- *  You should have received a copy of the GNU General Public License
- *  along with this program; if not, write to the Free Software
- *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
+ * Copyright (C) 1996-2023 The Squid Software Foundation and contributors
  *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
  */
+
+/* DEBUG: section 04    Error Generation */
+
 #include "squid.h"
+#include "AccessLogEntry.h"
 #include "cache_cf.h"
 #include "clients/forward.h"
 #include "comm/Connection.h"
 #include "comm/Write.h"
-#include "disk.h"
-#include "err_detail_type.h"
+#include "error/Detail.h"
+#include "error/SysErrorDetail.h"
 #include "errorpage.h"
 #include "fde.h"
+#include "format/Format.h"
+#include "fs_io.h"
 #include "html_quote.h"
 #include "HttpHeaderTools.h"
 #include "HttpReply.h"
 #include "MemBuf.h"
 #include "MemObject.h"
 #include "rfc1738.h"
+#include "sbuf/Stream.h"
 #include "SquidConfig.h"
 #include "Store.h"
 #include "tools.h"
-#include "URL.h"
 #include "wordlist.h"
 #if USE_AUTH
 #include "auth/UserRequest.h"
 #endif
-#include "SquidTime.h"
 #if USE_OPENSSL
 #include "ssl/ErrorDetailManager.h"
 #endif
 
+#include <array>
+
 /**
  \defgroup ErrorPageInternal Error Page Internals
  \ingroup ErrorPageAPI
  *
- \section Abstract Abstract:
+ \section ErrorPagesAbstract Abstract:
  *   These routines are used to generate error messages to be
  *   sent to clients.  The error type is used to select between
  *   the various message formats. (formats are stored in the
 /// \ingroup ErrorPageInternal
 CBDATA_CLASS_INIT(ErrorState);
 
+const SBuf ErrorState::LogformatMagic("@Squid{");
+
 /* local types */
 
-/// \ingroup ErrorPageInternal
-typedef struct {
+/// an error page created from admin-configurable metadata (e.g. deny_info)
+class ErrorDynamicPageInfo {
+public:
+    ErrorDynamicPageInfo(const int anId, const char *aName, const SBuf &aCfgLocation);
+    ~ErrorDynamicPageInfo() { xfree(page_name); }
+
+    /// error_text[] index for response body (unused in redirection responses)
     int id;
+
+    /// Primary deny_info parameter:
+    /// * May start with an HTTP status code.
+    /// * Either a well-known error page name, a filename, or a redirect URL.
     char *page_name;
+
+    /// admin-configured HTTP Location header value for redirection responses
+    const char *uri;
+
+    /// admin-configured name for the error page template (custom or standard)
+    const char *filename;
+
+    /// deny_info directive position in squid.conf (for reporting)
+    SBuf cfgLocation;
+
+    // XXX: Misnamed. Not just for redirects.
+    /// admin-configured HTTP status code
     Http::StatusCode page_redirect;
-} ErrorDynamicPageInfo;
 
-/* local constant and vars */
+private:
+    // no copying of any kind
+    ErrorDynamicPageInfo(ErrorDynamicPageInfo &&) = delete;
+};
 
-/**
- \ingroup ErrorPageInternal
- *
- \note  hard coded error messages are not appended with %S
- *      automagically to give you more control on the format
- */
-static const struct {
-    int type;                  /* and page_id */
-    const char *text;
+namespace ErrorPage {
+
+/// state and parameters shared by several ErrorState::compile*() methods
+class Build
+{
+public:
+    SBuf output; ///< compilation result
+    const char *input = nullptr; ///< template bytes that need to be compiled
+    bool building_deny_info_url = false; ///< whether we compile deny_info URI
+    bool allowRecursion = false; ///< whether top-level compile() calls are OK
+};
+
+/// pretty-prints error page/deny_info building error
+class BuildErrorPrinter
+{
+public:
+    BuildErrorPrinter(const SBuf &anInputLocation, int aPage, const char *aMsg, const char *aNear): inputLocation(anInputLocation), page_id(aPage), msg(aMsg), near(aNear) {}
+
+    /// reports error details (for admin-visible exceptions and debugging)
+    std::ostream &print(std::ostream &) const;
+
+    /// print() helper to report where the error was found
+    std::ostream &printLocation(std::ostream &os) const;
+
+    /* saved constructor parameters */
+    const SBuf &inputLocation;
+    const int page_id;
+    const char *msg;
+    const char *near;
+};
+
+static inline std::ostream &
+operator <<(std::ostream &os, const BuildErrorPrinter &context)
+{
+    return context.print(os);
 }
 
-error_hard_text[] = {
+static const char *IsDenyInfoUri(const int page_id);
 
+static void ImportStaticErrorText(const int page_id, const char *text, const SBuf &inputLocation);
+static void ValidateStaticError(const int page_id, const SBuf &inputLocation);
+
+} // namespace ErrorPage
+
+/* local constant and vars */
+
+/// an error page (or a part of an error page) with hard-coded template text
+class HardCodedError {
+public:
+    err_type type; ///< identifies the error (or a special error template part)
+    const char *text; ///< a string literal containing the error template
+};
+
+/// error messages that cannot be configured/customized externally
+static const std::array<HardCodedError, 7> HardCodedErrors = {
     {
-        ERR_SQUID_SIGNATURE,
-        "\n<br>\n"
-        "<hr>\n"
-        "<div id=\"footer\">\n"
-        "Generated %T by %h (%s)\n"
-        "</div>\n"
-        "</body></html>\n"
-    },
-    {
-        TCP_RESET,
-        "reset"
+        {
+            ERR_SQUID_SIGNATURE,
+            "\n<br>\n"
+            "<hr>\n"
+            "<div id=\"footer\">\n"
+            "Generated %T by %h (%s)\n"
+            "</div>\n"
+            "</body></html>\n"
+        },
+        {
+            TCP_RESET,
+            "reset"
+        },
+        {
+            ERR_CLIENT_GONE,
+            "unexpected client disconnect"
+        },
+        {
+            ERR_SECURE_ACCEPT_FAIL,
+            "secure accept fail"
+        },
+        {
+            ERR_REQUEST_START_TIMEOUT,
+            "request start timedout"
+        },
+        {
+            ERR_REQUEST_PARSE_TIMEOUT,
+            "request parse timedout"
+        },
+        {
+            ERR_RELAY_REMOTE,
+            "relay server response"
+        }
     }
 };
 
@@ -124,10 +196,7 @@ static std::vector<ErrorDynamicPageInfo *> ErrorDynamicPages;
 /* local prototypes */
 
 /// \ingroup ErrorPageInternal
-static const int error_hard_text_count = sizeof(error_hard_text) / sizeof(*error_hard_text);
-
-/// \ingroup ErrorPageInternal
-static char **error_text = NULL;
+static char **error_text = nullptr;
 
 /// \ingroup ErrorPageInternal
 static int error_page_count = 0;
@@ -136,8 +205,6 @@ static int error_page_count = 0;
 static MemBuf error_stylesheet;
 
 static const char *errorFindHardText(err_type type);
-static ErrorDynamicPageInfo *errorDynamicPageInfoCreate(int id, const char *page_name);
-static void errorDynamicPageInfoDestroy(ErrorDynamicPageInfo * info);
 static IOCB errorSendComplete;
 
 /// \ingroup ErrorPageInternal
@@ -145,24 +212,21 @@ static IOCB errorSendComplete;
 class ErrorPageFile: public TemplateFile
 {
 public:
-    ErrorPageFile(const char *name, const err_type code): TemplateFile(name,code) { textBuf.init();}
+    ErrorPageFile(const char *name, const err_type code) : TemplateFile(name, code) {}
 
     /// The template text data read from disk
-    const char *text() { return textBuf.content(); }
+    const char *text() { return template_.c_str(); }
 
-private:
-    /// stores the data read from disk to a local buffer
-    virtual bool parse(const char *buf, int len, bool eof) {
-        if (len)
-            textBuf.append(buf, len);
-        return true;
+protected:
+    void setDefault() override {
+        template_ = "Internal Error: Missing Template ";
+        template_.append(templateName.termedBuf());
     }
-
-    MemBuf textBuf; ///< A buffer to store the error page
 };
 
 /// \ingroup ErrorPageInternal
-err_type &operator++ (err_type &anErr)
+static err_type &
+operator++ (err_type &anErr)
 {
     int tmp = (int)anErr;
     anErr = (err_type)(++tmp);
@@ -170,14 +234,27 @@ err_type &operator++ (err_type &anErr)
 }
 
 /// \ingroup ErrorPageInternal
-int operator - (err_type const &anErr, err_type const &anErr2)
+static int
+operator -(err_type const &anErr, err_type const &anErr2)
 {
     return (int)anErr - (int)anErr2;
 }
 
+/// \return deny_info URL if the given page is a deny_info page with a URL
+/// \return nullptr otherwise
+static const char *
+ErrorPage::IsDenyInfoUri(const int page_id)
+{
+    if (ERR_MAX <= page_id && page_id < error_page_count)
+        return ErrorDynamicPages.at(page_id - ERR_MAX)->uri; // may be nil
+    return nullptr;
+}
+
 void
 errorInitialize(void)
 {
+    using ErrorPage::ImportStaticErrorText;
+
     err_type i;
     const char *text;
     error_page_count = ERR_MAX + ErrorDynamicPages.size();
@@ -190,7 +267,8 @@ errorInitialize(void)
             /**\par
              * Index any hard-coded error text into defaults.
              */
-            error_text[i] = xstrdup(text);
+            static const SBuf builtIn("built-in");
+            ImportStaticErrorText(i, text, builtIn);
 
         } else if (i < ERR_MAX) {
             /**\par
@@ -199,7 +277,8 @@ errorInitialize(void)
              *  (b) admin specified custom directory (error_directory)
              */
             ErrorPageFile errTmpl(err_type_str[i], i);
-            error_text[i] = errTmpl.loadDefault() ? xstrdup(errTmpl.text()) : NULL;
+            errTmpl.loadDefault();
+            ImportStaticErrorText(i, errTmpl.text(), errTmpl.filename);
         } else {
             /** \par
              * Index any unknown file names used by deny_info.
@@ -207,14 +286,14 @@ errorInitialize(void)
             ErrorDynamicPageInfo *info = ErrorDynamicPages.at(i - ERR_MAX);
             assert(info && info->id == i && info->page_name);
 
-            const char *pg = info->page_name;
-            if (info->page_redirect != Http::scNone)
-                pg = info->page_name +4;
-
-            if (strchr(pg, ':') == NULL) {
+            if (info->filename) {
                 /** But only if they are not redirection URL. */
-                ErrorPageFile errTmpl(pg, ERR_MAX);
-                error_text[i] = errTmpl.loadDefault() ? xstrdup(errTmpl.text()) : NULL;
+                ErrorPageFile errTmpl(info->filename, ERR_MAX);
+                errTmpl.loadDefault();
+                ImportStaticErrorText(i, errTmpl.text(), errTmpl.filename);
+            } else {
+                assert(info->uri);
+                ErrorPage::ValidateStaticError(i, info->cfgLocation);
             }
         }
     }
@@ -225,7 +304,7 @@ errorInitialize(void)
     if (Config.errorStylesheet) {
         ErrorPageFile tmpl("StylesSheet", ERR_MAX);
         tmpl.loadFromFile(Config.errorStylesheet);
-        error_stylesheet.Printf("%s",tmpl.text());
+        error_stylesheet.appendf("%s",tmpl.text());
     }
 
 #if USE_OPENSSL
@@ -246,7 +325,7 @@ errorClean(void)
     }
 
     while (!ErrorDynamicPages.empty()) {
-        errorDynamicPageInfoDestroy(ErrorDynamicPages.back());
+        delete ErrorDynamicPages.back();
         ErrorDynamicPages.pop_back();
     }
 
@@ -261,13 +340,11 @@ errorClean(void)
 static const char *
 errorFindHardText(err_type type)
 {
-    int i;
-
-    for (i = 0; i < error_hard_text_count; ++i)
-        if (error_hard_text[i].type == type)
-            return error_hard_text[i].text;
-
-    return NULL;
+    for (const auto &m: HardCodedErrors) {
+        if (m.type == type)
+            return m.text;
+    }
+    return nullptr;
 }
 
 TemplateFile::TemplateFile(const char *name, const err_type code): silent(false), wasLoaded(false), templateName(name), templateCode(code)
@@ -275,11 +352,11 @@ TemplateFile::TemplateFile(const char *name, const err_type code): silent(false)
     assert(name);
 }
 
-bool
+void
 TemplateFile::loadDefault()
 {
     if (loaded()) // already loaded?
-        return true;
+        return;
 
     /** test error_directory configured location */
     if (Config.errorDirectory) {
@@ -292,7 +369,7 @@ TemplateFile::loadDefault()
     /** test error_default_language location */
     if (!loaded() && Config.errorDefaultLanguage) {
         if (!tryLoadTemplate(Config.errorDefaultLanguage)) {
-            debugs(1, (templateCode < TCP_RESET ? DBG_CRITICAL : 3), "Unable to load default error language files. Reset to backups.");
+            debugs(1, (templateCode < TCP_RESET ? DBG_CRITICAL : 3), "ERROR: Unable to load default error language files. Reset to backups.");
         }
     }
 #endif
@@ -305,11 +382,10 @@ TemplateFile::loadDefault()
     /* giving up if failed */
     if (!loaded()) {
         debugs(1, (templateCode < TCP_RESET ? DBG_CRITICAL : 3), "WARNING: failed to find or read error text file " << templateName);
-        parse("Internal Error: Missing Template ", 33, '\0');
-        parse(templateName.termedBuf(), templateName.size(), '\0');
+        template_.clear();
+        setDefault();
+        wasLoaded = true;
     }
-
-    return true;
 }
 
 bool
@@ -330,7 +406,7 @@ TemplateFile::tryLoadTemplate(const char *lang)
     if ( strlen(lang) == 2) {
         /* TODO glob the error directory for sub-dirs matching: <tag> '-*'   */
         /* use first result. */
-        debugs(4,2, HERE << "wildcard fallback errors not coded yet.");
+        debugs(4,2, "wildcard fallback errors not coded yet.");
     }
 #endif
 
@@ -351,27 +427,37 @@ TemplateFile::loadFromFile(const char *path)
 
     if (fd < 0) {
         /* with dynamic locale negotiation we may see some failures before a success. */
-        if (!silent && templateCode < TCP_RESET)
-            debugs(4, DBG_CRITICAL, HERE << "'" << path << "': " << xstrerror());
+        if (!silent && templateCode < TCP_RESET) {
+            int xerrno = errno;
+            debugs(4, DBG_CRITICAL, "ERROR: loading file '" << path << "': " << xstrerr(xerrno));
+        }
         wasLoaded = false;
         return wasLoaded;
     }
 
+    template_.clear();
     while ((len = FD_READ_METHOD(fd, buf, sizeof(buf))) > 0) {
-        if (!parse(buf, len, false)) {
-            debugs(4, DBG_CRITICAL, HERE << " parse error while reading template file: " << path);
-            wasLoaded = false;
-            return wasLoaded;
-        }
+        template_.append(buf, len);
     }
-    parse(buf, 0, true);
 
     if (len < 0) {
-        debugs(4, DBG_CRITICAL, HERE << "failed to fully read: '" << path << "': " << xstrerror());
+        int xerrno = errno;
+        file_close(fd);
+        debugs(4, DBG_CRITICAL, MYNAME << "ERROR: failed to fully read: '" << path << "': " << xstrerr(xerrno));
+        wasLoaded = false;
+        return false;
     }
 
     file_close(fd);
 
+    filename = SBuf(path);
+
+    if (!parse()) {
+        debugs(4, DBG_CRITICAL, "ERROR: parsing error in template file: " << path);
+        wasLoaded = false;
+        return false;
+    }
+
     wasLoaded = true;
     return wasLoaded;
 }
@@ -379,7 +465,6 @@ TemplateFile::loadFromFile(const char *path)
 bool strHdrAcptLangGetItem(const String &hdr, char *lang, int langLen, size_t &pos)
 {
     while (pos < hdr.size()) {
-        char *dt = lang;
 
         /* skip any initial whitespace. */
         while (pos < hdr.size() && xisspace(hdr[pos]))
@@ -393,6 +478,7 @@ bool strHdrAcptLangGetItem(const String &hdr, char *lang, int langLen, size_t &p
          *    with preference given to an exact match.
          */
         bool invalid_byte = false;
+        char *dt = lang;
         while (pos < hdr.size() && hdr[pos] != ';' && hdr[pos] != ',' && !xisspace(hdr[pos]) && dt < (lang + (langLen -1)) ) {
             if (!invalid_byte) {
 #if USE_HTTP_VIOLATIONS
@@ -412,7 +498,6 @@ bool strHdrAcptLangGetItem(const String &hdr, char *lang, int langLen, size_t &p
             ++pos;
         }
         *dt = '\0'; // nul-terminated the filename content string before system use.
-        ++dt;
 
         // if we terminated the tag on garbage or ';' we need to skip to the next ',' or end of header.
         while (pos < hdr.size() && hdr[pos] != ',')
@@ -421,7 +506,7 @@ bool strHdrAcptLangGetItem(const String &hdr, char *lang, int langLen, size_t &p
         if (pos < hdr.size() && hdr[pos] == ',')
             ++pos;
 
-        debugs(4, 9, HERE << "STATE: dt='" << dt << "', lang='" << lang << "', pos=" << pos << ", buf='" << ((pos < hdr.size()) ? hdr.substr(pos,hdr.size()) : "") << "'");
+        debugs(4, 9, "STATE: lang=" << lang << ", pos=" << pos << ", buf='" << ((pos < hdr.size()) ? hdr.substr(pos,hdr.size()) : "") << "'");
 
         /* if we found anything we might use, try it. */
         if (*lang != '\0' && !invalid_byte)
@@ -439,23 +524,23 @@ TemplateFile::loadFor(const HttpRequest *request)
     if (loaded()) // already loaded?
         return true;
 
-    if (!request || !request->header.getList(HDR_ACCEPT_LANGUAGE, &hdr) )
+    if (!request || !request->header.getList(Http::HdrType::ACCEPT_LANGUAGE, &hdr))
         return false;
 
     char lang[256];
     size_t pos = 0; // current parsing position in header string
 
-    debugs(4, 6, HERE << "Testing Header: '" << hdr << "'");
+    debugs(4, 6, "Testing Header: '" << hdr << "'");
 
     while ( strHdrAcptLangGetItem(hdr, lang, 256, pos) ) {
 
         /* wildcard uses the configured default language */
         if (lang[0] == '*' && lang[1] == '\0') {
-            debugs(4, 6, HERE << "Found language '" << lang << "'. Using configured default.");
+            debugs(4, 6, "Found language '" << lang << "'. Using configured default.");
             return false;
         }
 
-        debugs(4, 6, HERE << "Found language '" << lang << "', testing for available template");
+        debugs(4, 6, "Found language '" << lang << "', testing for available template");
 
         if (tryLoadTemplate(lang)) {
             /* store the language we found for the Content-Language reply header */
@@ -465,19 +550,48 @@ TemplateFile::loadFor(const HttpRequest *request)
             debugs(4, DBG_IMPORTANT, "WARNING: Error Pages Missing Language: " << lang);
         }
     }
+#else
+    (void)request;
 #endif
 
     return loaded();
 }
 
-/// \ingroup ErrorPageInternal
-static ErrorDynamicPageInfo *
-errorDynamicPageInfoCreate(int id, const char *page_name)
+ErrorDynamicPageInfo::ErrorDynamicPageInfo(const int anId, const char *aName, const SBuf &aCfgLocation):
+    id(anId),
+    page_name(xstrdup(aName)),
+    uri(nullptr),
+    filename(nullptr),
+    cfgLocation(aCfgLocation),
+    page_redirect(static_cast<Http::StatusCode>(atoi(page_name)))
 {
-    ErrorDynamicPageInfo *info = new ErrorDynamicPageInfo;
-    info->id = id;
-    info->page_name = xstrdup(page_name);
-    info->page_redirect = static_cast<Http::StatusCode>(atoi(page_name));
+    const char *filenameOrUri = nullptr;
+    if (xisdigit(*page_name)) {
+        if (const char *statusCodeEnd = strchr(page_name, ':'))
+            filenameOrUri = statusCodeEnd + 1;
+    } else {
+        assert(!page_redirect);
+        filenameOrUri = page_name;
+    }
+
+    // Guessed uri, filename, or both values may be nil or malformed.
+    // They are validated later.
+    if (!page_redirect) {
+        if (filenameOrUri && strchr(filenameOrUri, ':')) // looks like a URL
+            uri = filenameOrUri;
+        else
+            filename = filenameOrUri;
+    }
+    else if (page_redirect/100 == 3) {
+        // redirects imply a URL
+        uri = filenameOrUri;
+    } else {
+        // non-redirects imply an error page name
+        filename = filenameOrUri;
+    }
+
+    const auto info = this; // source code change reduction hack
+    // TODO: Move and refactor to avoid self_destruct()s in reconfigure.
 
     /* WARNING on redirection status:
      * 2xx are permitted, but not documented officially.
@@ -510,17 +624,6 @@ errorDynamicPageInfoCreate(int id, const char *page_name)
         self_destruct();
     }
     // else okay.
-
-    return info;
-}
-
-/// \ingroup ErrorPageInternal
-static void
-errorDynamicPageInfoDestroy(ErrorDynamicPageInfo * info)
-{
-    assert(info);
-    safe_free(info->page_name);
-    delete info;
 }
 
 /// \ingroup ErrorPageInternal
@@ -541,15 +644,14 @@ errorPageId(const char *page_name)
 }
 
 err_type
-errorReservePageId(const char *page_name)
+errorReservePageId(const char *page_name, const SBuf &cfgLocation)
 {
-    ErrorDynamicPageInfo *info;
     int id = errorPageId(page_name);
 
     if (id == ERR_NONE) {
-        info = errorDynamicPageInfoCreate(ERR_MAX + ErrorDynamicPages.size(), page_name);
+        id = ERR_MAX + ErrorDynamicPages.size();
+        const auto info = new ErrorDynamicPageInfo(id, page_name, cfgLocation);
         ErrorDynamicPages.push_back(info);
-        id = info->id;
     }
 
     return (err_type)id;
@@ -559,57 +661,55 @@ errorReservePageId(const char *page_name)
 const char *
 errorPageName(int pageId)
 {
-    if (pageId >= ERR_NONE && pageId < ERR_MAX)                /* common case */
+    if (pageId >= ERR_NONE && pageId < ERR_MAX)     /* common case */
         return err_type_str[pageId];
 
     if (pageId >= ERR_MAX && pageId - ERR_MAX < (ssize_t)ErrorDynamicPages.size())
         return ErrorDynamicPages[pageId - ERR_MAX]->page_name;
 
-    return "ERR_UNKNOWN";      /* should not happen */
+    return "ERR_UNKNOWN";   /* should not happen */
 }
 
 ErrorState *
-ErrorState::NewForwarding(err_type type, HttpRequest *request)
+ErrorState::NewForwarding(err_type type, HttpRequestPointer &request, const AccessLogEntry::Pointer &ale)
 {
-    assert(request);
-    const Http::StatusCode status = request->flags.needValidation ?
+    const Http::StatusCode status = (request && request->flags.needValidation) ?
                                     Http::scGatewayTimeout : Http::scServiceUnavailable;
-    return new ErrorState(type, status, request);
+    return new ErrorState(type, status, request.getRaw(), ale);
 }
 
-ErrorState::ErrorState(err_type t, Http::StatusCode status, HttpRequest * req) :
-        type(t),
-        page_id(t),
-        err_language(NULL),
-        httpStatus(status),
-#if USE_AUTH
-        auth_user_request (NULL),
-#endif
-        request(NULL),
-        url(NULL),
-        xerrno(0),
-        port(0),
-        dnsError(),
-        ttl(0),
-        src_addr(),
-        redirect_url(NULL),
-        callback(NULL),
-        callback_data(NULL),
-        request_hdrs(NULL),
-        err_msg(NULL),
-#if USE_OPENSSL
-        detail(NULL),
-#endif
-        detailCode(ERR_DETAIL_NONE)
+ErrorState::ErrorState(err_type t) :
+    type(t),
+    page_id(t),
+    callback(nullptr)
 {
-    memset(&ftp, 0, sizeof(ftp));
+}
 
+ErrorState::ErrorState(err_type t, Http::StatusCode status, HttpRequest * req, const AccessLogEntry::Pointer &anAle) :
+    ErrorState(t)
+{
     if (page_id >= ERR_MAX && ErrorDynamicPages[page_id - ERR_MAX]->page_redirect != Http::scNone)
         httpStatus = ErrorDynamicPages[page_id - ERR_MAX]->page_redirect;
+    else
+        httpStatus = status;
 
-    if (req != NULL) {
+    if (req) {
+        request = req;
+        src_addr = req->client_addr;
+    }
+
+    ale = anAle;
+}
+
+ErrorState::ErrorState(HttpRequest * req, HttpReply *errorReply) :
+    ErrorState(ERR_RELAY_REMOTE)
+{
+    Must(errorReply);
+    response_ = errorReply;
+    httpStatus = errorReply->sline.status();
+
+    if (req) {
         request = req;
-        HTTPMSGLOCK(request);
         src_addr = req->client_addr;
     }
 }
@@ -617,11 +717,9 @@ ErrorState::ErrorState(err_type t, Http::StatusCode status, HttpRequest * req) :
 void
 errorAppendEntry(StoreEntry * entry, ErrorState * err)
 {
-    assert(entry->mem_obj != NULL);
+    assert(entry->mem_obj != nullptr);
     assert (entry->isEmpty());
-    debugs(4, 4, "Creating an error page for entry " << entry <<
-           " with errorstate " << err <<
-           " page id " << err->page_id);
+    debugs(4, 4, "storing " << err << " in " << *entry);
 
     if (entry->store_status != STORE_PENDING) {
         debugs(4, 2, "Skipping error page due to store_status: " << entry->store_status);
@@ -650,19 +748,16 @@ errorAppendEntry(StoreEntry * entry, ErrorState * err)
 void
 errorSend(const Comm::ConnectionPointer &conn, ErrorState * err)
 {
-    HttpReply *rep;
-    debugs(4, 3, HERE << conn << ", err=" << err);
+    debugs(4, 3, conn << ", err=" << err);
     assert(Comm::IsConnOpen(conn));
 
-    rep = err->BuildHttpReply();
+    HttpReplyPointer rep(err->BuildHttpReply());
 
     MemBuf *mb = rep->pack();
     AsyncCall::Pointer call = commCbCall(78, 5, "errorSendComplete",
                                          CommIoCbPtrFun(&errorSendComplete, err));
     Comm::Write(conn, mb, call);
     delete mb;
-
-    delete rep;
 }
 
 /**
@@ -675,10 +770,10 @@ errorSend(const Comm::ConnectionPointer &conn, ErrorState * err)
  *     closing the FD, otherwise we do it ourselves.
  */
 static void
-errorSendComplete(const Comm::ConnectionPointer &conn, char *bufnotused, size_t size, Comm::Flag errflag, int xerrno, void *data)
+errorSendComplete(const Comm::ConnectionPointer &conn, char *, size_t size, Comm::Flag errflag, int, void *data)
 {
     ErrorState *err = static_cast<ErrorState *>(data);
-    debugs(4, 3, HERE << conn << ", size=" << size);
+    debugs(4, 3, conn << ", size=" << size);
 
     if (errflag != Comm::ERR_CLOSING) {
         if (err->callback) {
@@ -695,24 +790,17 @@ errorSendComplete(const Comm::ConnectionPointer &conn, char *bufnotused, size_t
 
 ErrorState::~ErrorState()
 {
-    HTTPMSGUNLOCK(request);
     safe_free(redirect_url);
     safe_free(url);
     safe_free(request_hdrs);
     wordlistDestroy(&ftp.server_msg);
     safe_free(ftp.request);
     safe_free(ftp.reply);
-#if USE_AUTH
-    auth_user_request = NULL;
-#endif
     safe_free(err_msg);
 #if USE_ERR_LOCALES
     if (err_language != Config.errorDefaultLanguage)
 #endif
         safe_free(err_language);
-#if USE_OPENSSL
-    delete detail;
-#endif
 }
 
 int
@@ -723,72 +811,61 @@ ErrorState::Dump(MemBuf * mb)
 
     str.reset();
     /* email subject line */
-    str.Printf("CacheErrorInfo - %s", errorPageName(type));
-    mb->Printf("?subject=%s", rfc1738_escape_part(str.buf));
+    str.appendf("CacheErrorInfo - %s", errorPageName(type));
+    mb->appendf("?subject=%s", rfc1738_escape_part(str.buf));
     str.reset();
     /* email body */
-    str.Printf("CacheHost: %s\r\n", getMyHostname());
+    str.appendf("CacheHost: %s\r\n", getMyHostname());
     /* - Err Msgs */
-    str.Printf("ErrPage: %s\r\n", errorPageName(type));
+    str.appendf("ErrPage: %s\r\n", errorPageName(type));
 
     if (xerrno) {
-        str.Printf("Err: (%d) %s\r\n", xerrno, strerror(xerrno));
+        str.appendf("Err: (%d) %s\r\n", xerrno, strerror(xerrno));
     } else {
-        str.Printf("Err: [none]\r\n");
+        str.append("Err: [none]\r\n", 13);
     }
 #if USE_AUTH
-    if (auth_user_request->denyMessage())
-        str.Printf("Auth ErrMsg: %s\r\n", auth_user_request->denyMessage());
+    if (auth_user_request.getRaw() && auth_user_request->denyMessage())
+        str.appendf("Auth ErrMsg: %s\r\n", auth_user_request->denyMessage());
 #endif
     if (dnsError.size() > 0)
-        str.Printf("DNS ErrMsg: %s\r\n", dnsError.termedBuf());
+        str.appendf("DNS ErrMsg: %s\r\n", dnsError.termedBuf());
 
     /* - TimeStamp */
-    str.Printf("TimeStamp: %s\r\n\r\n", mkrfc1123(squid_curtime));
+    str.appendf("TimeStamp: %s\r\n\r\n", Time::FormatRfc1123(squid_curtime));
 
     /* - IP stuff */
-    str.Printf("ClientIP: %s\r\n", src_addr.toStr(ntoabuf,MAX_IPSTRLEN));
+    str.appendf("ClientIP: %s\r\n", src_addr.toStr(ntoabuf,MAX_IPSTRLEN));
 
     if (request && request->hier.host[0] != '\0') {
-        str.Printf("ServerIP: %s\r\n", request->hier.host);
+        str.appendf("ServerIP: %s\r\n", request->hier.host);
     }
 
-    str.Printf("\r\n");
+    str.append("\r\n", 2);
     /* - HTTP stuff */
-    str.Printf("HTTP Request:\r\n");
-
-    if (NULL != request) {
-        Packer pck;
-        String urlpath_or_slash;
-
-        if (request->urlpath.size() != 0)
-            urlpath_or_slash = request->urlpath;
-        else
-            urlpath_or_slash = "/";
-
-        str.Printf(SQUIDSBUFPH " " SQUIDSTRINGPH " %s/%d.%d\n",
-                   SQUIDSBUFPRINT(request->method.image()),
-                   SQUIDSTRINGPRINT(urlpath_or_slash),
-                   AnyP::ProtocolType_str[request->http_ver.protocol],
-                   request->http_ver.major, request->http_ver.minor);
-        packerToMemInit(&pck, &str);
-        request->header.packInto(&pck);
-        packerClean(&pck);
+    str.append("HTTP Request:\r\n", 15);
+    if (request) {
+        str.appendf(SQUIDSBUFPH " " SQUIDSBUFPH " %s/%d.%d\n",
+                    SQUIDSBUFPRINT(request->method.image()),
+                    SQUIDSBUFPRINT(request->url.path()),
+                    AnyP::ProtocolType_str[request->http_ver.protocol],
+                    request->http_ver.major, request->http_ver.minor);
+        request->header.packInto(&str);
     }
 
-    str.Printf("\r\n");
+    str.append("\r\n", 2);
     /* - FTP stuff */
 
     if (ftp.request) {
-        str.Printf("FTP Request: %s\r\n", ftp.request);
-        str.Printf("FTP Reply: %s\r\n", (ftp.reply? ftp.reply:"[none]"));
-        str.Printf("FTP Msg: ");
+        str.appendf("FTP Request: %s\r\n", ftp.request);
+        str.appendf("FTP Reply: %s\r\n", (ftp.reply? ftp.reply:"[none]"));
+        str.append("FTP Msg: ", 9);
         wordlistCat(ftp.server_msg, &str);
-        str.Printf("\r\n");
+        str.append("\r\n", 2);
     }
 
-    str.Printf("\r\n");
-    mb->Printf("&body=%s", rfc1738_escape_part(str.buf));
+    str.append("\r\n", 2);
+    mb->appendf("&body=%s", rfc1738_escape_part(str.buf));
     str.clean();
     return 0;
 }
@@ -796,35 +873,87 @@ ErrorState::Dump(MemBuf * mb)
 /// \ingroup ErrorPageInternal
 #define CVT_BUF_SZ 512
 
-const char *
-ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion)
+void
+ErrorState::compileLogformatCode(Build &build)
+{
+    assert(LogformatMagic.cmp(build.input, LogformatMagic.length()) == 0);
+
+    try {
+        const auto logformat = build.input + LogformatMagic.length();
+
+        // Logformat supports undocumented "external" encoding specifications
+        // like [%>h] or "%<a". To preserve the possibility of extending
+        // @Squid{} syntax to non-logformat sequences, we require logformat
+        // sequences to start with '%'. This restriction does not limit
+        // logformat quoting abilities. TODO: Deprecate "external" encoding?
+        if (*logformat != '%')
+            throw TexcHere("logformat expressions that do not start with % are not supported");
+
+        static MemBuf result;
+        result.reset();
+        const auto logformatLen = Format::AssembleOne(logformat, result, ale);
+        assert(logformatLen > 0);
+        const auto closure = logformat + logformatLen;
+        if (*closure != '}')
+            throw TexcHere("Missing closing brace (})");
+        build.output.append(result.content(), result.contentSize());
+        build.input = closure + 1;
+        return;
+    } catch (...) {
+        noteBuildError("Bad @Squid{logformat} sequence", build.input);
+    }
+
+    // we cannot recover reliably so stop interpreting the rest of input
+    const auto remainingSize = strlen(build.input);
+    build.output.append(build.input, remainingSize);
+    build.input += remainingSize;
+}
+
+void
+ErrorState::compileLegacyCode(Build &build)
 {
     static MemBuf mb;
-    const char *p = NULL;      /* takes priority over mb if set */
+    const char *p = nullptr;   /* takes priority over mb if set */
     int do_quote = 1;
     int no_urlescape = 0;       /* if true then item is NOT to be further URL-encoded */
     char ntoabuf[MAX_IPSTRLEN];
 
     mb.reset();
 
-    switch (token) {
+    const auto &building_deny_info_url = build.building_deny_info_url; // a change reduction hack
+
+    const auto letter = build.input[1];
+
+    switch (letter) {
 
     case 'a':
 #if USE_AUTH
-        if (request && request->auth_user_request != NULL)
+        if (request && request->auth_user_request)
             p = request->auth_user_request->username();
         if (!p)
 #endif
             p = "-";
         break;
 
+    case 'A':
+        // TODO: When/if we get ALE here, pass it as well
+        if (const auto addr = FindListeningPortAddress(request.getRaw(), nullptr))
+            mb.appendf("%s", addr->toStr(ntoabuf, MAX_IPSTRLEN));
+        else
+            p = "-";
+        break;
+
     case 'b':
-        mb.Printf("%d", getMyPort());
+        mb.appendf("%u", getMyPort());
         break;
 
     case 'B':
         if (building_deny_info_url) break;
-        p = request ? Ftp::UrlWith2f(request) : "[no URL]";
+        if (request) {
+            const SBuf &tmp = Ftp::UrlWith2f(request.getRaw());
+            mb.append(tmp.rawContent(), tmp.length());
+        } else
+            p = "[no URL]";
         break;
 
     case 'c':
@@ -833,34 +962,28 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
         break;
 
     case 'D':
-        if (!allowRecursion)
+        if (!build.allowRecursion)
             p = "%D";  // if recursion is not allowed, do not convert
-#if USE_OPENSSL
-        // currently only SSL error details implemented
         else if (detail) {
-            detail->useRequest(request);
-            const String &errDetail = detail->toString();
-            if (errDetail.size() > 0) {
-                MemBuf *detail_mb  = ConvertText(errDetail.termedBuf(), false);
-                mb.append(detail_mb->content(), detail_mb->contentSize());
-                delete detail_mb;
-                do_quote = 0;
-            }
+            auto rawDetail = detail->verbose(request);
+            // XXX: Performance regression. c_str() reallocates
+            const auto compiledDetail = compileBody(rawDetail.c_str(), false);
+            mb.append(compiledDetail.rawContent(), compiledDetail.length());
+            do_quote = 0;
         }
-#endif
         if (!mb.contentSize())
-            mb.Printf("[No Error Detail]");
+            mb.append("[No Error Detail]", 17);
         break;
 
     case 'e':
-        mb.Printf("%d", xerrno);
+        mb.appendf("%d", xerrno);
         break;
 
     case 'E':
         if (xerrno)
-            mb.Printf("(%d) %s", xerrno, strerror(xerrno));
+            mb.appendf("(%d) %s", xerrno, strerror(xerrno));
         else
-            mb.Printf("[No Error]");
+            mb.append("[No Error]", 10);
         break;
 
     case 'f':
@@ -893,7 +1016,7 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
         break;
 
     case 'h':
-        mb.Printf("%s", getMyHostname());
+        mb.appendf("%s", getMyHostname());
         break;
 
     case 'H':
@@ -901,17 +1024,17 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
             if (request->hier.host[0] != '\0') // if non-empty string.
                 p = request->hier.host;
             else
-                p = request->GetHost();
+                p = request->url.host();
         } else if (!building_deny_info_url)
             p = "[unknown host]";
         break;
 
     case 'i':
-        mb.Printf("%s", src_addr.toStr(ntoabuf,MAX_IPSTRLEN));
+        mb.appendf("%s", src_addr.toStr(ntoabuf,MAX_IPSTRLEN));
         break;
 
     case 'I':
-        if (request && request->hier.tcpServer != NULL)
+        if (request && request->hier.tcpServer)
             p = request->hier.tcpServer->remote.toStr(ntoabuf,MAX_IPSTRLEN);
         else if (!building_deny_info_url)
             p = "[unknown]";
@@ -926,7 +1049,7 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
     case 'L':
         if (building_deny_info_url) break;
         if (Config.errHtmlText) {
-            mb.Printf("%s", Config.errHtmlText);
+            mb.appendf("%s", Config.errHtmlText);
             do_quote = 0;
         } else
             p = "[not available]";
@@ -935,7 +1058,10 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
     case 'm':
         if (building_deny_info_url) break;
 #if USE_AUTH
-        p = auth_user_request->denyMessage("[not available]");
+        if (auth_user_request.getRaw())
+            p = auth_user_request->denyMessage("[not available]");
+        else
+            p = "[not available]";
 #else
         p = "-";
 #endif
@@ -949,6 +1075,10 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
             p = "[unknown method]";
         break;
 
+    case 'O':
+        if (!building_deny_info_url)
+            do_quote = 0;
+        [[fallthrough]];
     case 'o':
         p = request ? request->extacl_message.termedBuf() : external_acl_message;
         if (!p && !building_deny_info_url)
@@ -957,7 +1087,7 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
 
     case 'p':
         if (request) {
-            mb.Printf("%d", (int) request->port);
+            mb.appendf("%u", request->url.port());
         } else if (!building_deny_info_url) {
             p = "[unknown port]";
         }
@@ -965,7 +1095,8 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
 
     case 'P':
         if (request) {
-            p = request->url.getScheme().c_str();
+            const SBuf &m = request->url.getScheme().image();
+            mb.append(m.rawContent(), m.length());
         } else if (!building_deny_info_url) {
             p = "[unknown protocol]";
         }
@@ -973,27 +1104,21 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
 
     case 'R':
         if (building_deny_info_url) {
-            p = (request->urlpath.size() != 0 ? request->urlpath.termedBuf() : "/");
-            no_urlescape = 1;
+            if (request != nullptr) {
+                const SBuf &tmp = request->url.path();
+                mb.append(tmp.rawContent(), tmp.length());
+                no_urlescape = 1;
+            } else
+                p = "[no request]";
             break;
         }
-        if (NULL != request) {
-            Packer pck;
-            String urlpath_or_slash;
-
-            if (request->urlpath.size() != 0)
-                urlpath_or_slash = request->urlpath;
-            else
-                urlpath_or_slash = "/";
-
-            mb.Printf(SQUIDSBUFPH " " SQUIDSTRINGPH " %s/%d.%d\n",
-                      SQUIDSBUFPRINT(request->method.image()),
-                      SQUIDSTRINGPRINT(urlpath_or_slash),
-                      AnyP::ProtocolType_str[request->http_ver.protocol],
-                      request->http_ver.major, request->http_ver.minor);
-            packerToMemInit(&pck, &mb);
-            request->header.packInto(&pck, true); //hide authorization data
-            packerClean(&pck);
+        if (request) {
+            mb.appendf(SQUIDSBUFPH " " SQUIDSBUFPH " %s/%d.%d\n",
+                       SQUIDSBUFPRINT(request->method.image()),
+                       SQUIDSBUFPRINT(request->url.path()),
+                       AnyP::ProtocolType_str[request->http_ver.protocol],
+                       request->http_ver.major, request->http_ver.minor);
+            request->header.packInto(&mb, true); //hide authorization data
         } else if (request_hdrs) {
             p = request_hdrs;
         } else {
@@ -1004,7 +1129,11 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
     case 's':
         /* for backward compat we make %s show the full URL. Drop this in some future release. */
         if (building_deny_info_url) {
-            p = request ? urlCanonical(request) : url;
+            if (request) {
+                const SBuf &tmp = request->effectiveRequestUri();
+                mb.append(tmp.rawContent(), tmp.length());
+            } else
+                p = url;
             debugs(0, DBG_CRITICAL, "WARNING: deny_info now accepts coded tags. Use %u to get the full URL instead of %s");
         } else
             p = visible_appname_string;
@@ -1019,10 +1148,8 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
         if (page_id != ERR_SQUID_SIGNATURE) {
             const int saved_id = page_id;
             page_id = ERR_SQUID_SIGNATURE;
-            MemBuf *sign_mb = BuildContent();
-            mb.Printf("%s", sign_mb->content());
-            sign_mb->clean();
-            delete sign_mb;
+            const auto signature = buildBody();
+            mb.append(signature.rawContent(), signature.length());
             page_id = saved_id;
             do_quote = 0;
         } else {
@@ -1032,18 +1159,18 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
         break;
 
     case 't':
-        mb.Printf("%s", Time::FormatHttpd(squid_curtime));
+        mb.appendf("%s", Time::FormatHttpd(squid_curtime));
         break;
 
     case 'T':
-        mb.Printf("%s", mkrfc1123(squid_curtime));
+        mb.appendf("%s", Time::FormatRfc1123(squid_curtime));
         break;
 
     case 'U':
-        /* Using the fake-https version of canonical so error pages see https:// */
+        /* Using the fake-https version of absolute-URI so error pages see https:// */
         /* even when the url-path cannot be shown as more than '*' */
         if (request)
-            p = urlCanonicalFakeHttps(request);
+            p = urlCanonicalFakeHttps(request.getRaw());
         else if (url)
             p = url;
         else if (!building_deny_info_url)
@@ -1051,9 +1178,10 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
         break;
 
     case 'u':
-        if (request)
-            p = urlCanonical(request);
-        else if (url)
+        if (request) {
+            const SBuf &tmp = request->effectiveRequestUri();
+            mb.append(tmp.rawContent(), tmp.length());
+        } else if (url)
             p = url;
         else if (!building_deny_info_url)
             p = "[no URL]";
@@ -1061,7 +1189,7 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
 
     case 'w':
         if (Config.adminEmail)
-            mb.Printf("%s", Config.adminEmail);
+            mb.appendf("%s", Config.adminEmail);
         else if (!building_deny_info_url)
             p = "[unknown]";
         break;
@@ -1074,13 +1202,12 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
         break;
 
     case 'x':
-#if USE_OPENSSL
-        if (detail)
-            mb.Printf("%s", detail->errorName());
-        else
-#endif
-            if (!building_deny_info_url)
-                p = "[Unknown Error Code]";
+        if (detail) {
+            const auto brief = detail->brief();
+            mb.append(brief.rawContent(), brief.length());
+        } else if (!building_deny_info_url) {
+            p = "[Unknown Error Code]";
+        }
         break;
 
     case 'z':
@@ -1106,17 +1233,23 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
         break;
 
     default:
-        mb.Printf("%%%c", token);
+        if (building_deny_info_url)
+            bypassBuildErrorXXX("Unsupported deny_info %code", build.input);
+        else if (letter != ';')
+            bypassBuildErrorXXX("Unsupported error page %code", build.input);
+        // else too many "font-size: 100%;" template errors to report
+
+        mb.append(build.input, 2);
         do_quote = 0;
         break;
     }
 
     if (!p)
-        p = mb.buf;            /* do not use mb after this assignment! */
+        p = mb.buf;     /* do not use mb after this assignment! */
 
     assert(p);
 
-    debugs(4, 3, "errorConvert: %%" << token << " --> '" << p << "'" );
+    debugs(4, 3, "%" << letter << " --> '" << p << "'" );
 
     if (do_quote)
         p = html_quote(p);
@@ -1124,40 +1257,34 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion
     if (building_deny_info_url && !no_urlescape)
         p = rfc1738_escape_part(p);
 
-    return p;
+    // TODO: Optimize by replacing mb with direct build.output usage.
+    build.output.append(p, strlen(p));
+    build.input += 2;
 }
 
 void
-ErrorState::DenyInfoLocation(const char *name, HttpRequest *aRequest, MemBuf &result)
+ErrorState::validate()
 {
-    char const *m = name;
-    char const *p = m;
-    char const *t;
-
-    if (m[0] == '3')
-        m += 4; // skip "3xx:"
-
-    while ((p = strchr(m, '%'))) {
-        result.append(m, p - m);       /* copy */
-        t = Convert(*++p, true, true);       /* convert */
-        result.Printf("%s", t);        /* copy */
-        m = p + 1;                     /* advance */
+    if (const auto urlTemplate = ErrorPage::IsDenyInfoUri(page_id)) {
+        (void)compile(urlTemplate, true, true);
+    } else {
+        assert(page_id > ERR_NONE);
+        assert(page_id < error_page_count);
+        (void)compileBody(error_text[page_id], true);
     }
-
-    if (*m)
-        result.Printf("%s", m);        /* copy tail */
-
-    assert((size_t)result.contentSize() == strlen(result.content()));
 }
 
 HttpReply *
 ErrorState::BuildHttpReply()
 {
+    if (response_)
+        return response_.getRaw();
+
     HttpReply *rep = new HttpReply;
     const char *name = errorPageName(page_id);
     /* no LMT for error pages; error pages expire immediately */
 
-    if (name[0] == '3' || (name[0] != '2' && name[0] != '4' && name[0] != '5' && strchr(name, ':'))) {
+    if (const auto urlTemplate = ErrorPage::IsDenyInfoUri(page_id)) {
         /* Redirection */
         Http::StatusCode status = Http::scFound;
         // Use configured 3xx reply status if set.
@@ -1165,23 +1292,21 @@ ErrorState::BuildHttpReply()
             status = httpStatus;
         else {
             // Use 307 for HTTP/1.1 non-GET/HEAD requests.
-            if (request->method != Http::METHOD_GET && request->method != Http::METHOD_HEAD && request->http_ver >= Http::ProtocolVersion(1,1))
+            if (request && request->method != Http::METHOD_GET && request->method != Http::METHOD_HEAD && request->http_ver >= Http::ProtocolVersion(1,1))
                 status = Http::scTemporaryRedirect;
         }
 
-        rep->setHeaders(status, NULL, "text/html;charset=utf-8", 0, 0, -1);
+        rep->setHeaders(status, nullptr, "text/html;charset=utf-8", 0, 0, -1);
 
         if (request) {
-            MemBuf redirect_location;
-            redirect_location.init();
-            DenyInfoLocation(name, request, redirect_location);
-            httpHeaderPutStrf(&rep->header, HDR_LOCATION, "%s", redirect_location.content() );
+            auto location = compile(urlTemplate, true, true);
+            rep->header.putStr(Http::HdrType::LOCATION, location.c_str());
         }
 
-        httpHeaderPutStrf(&rep->header, HDR_X_SQUID_ERROR, "%d %s", httpStatus, "Access Denied");
+        httpHeaderPutStrf(&rep->header, Http::HdrType::X_SQUID_ERROR, "%d %s", httpStatus, "Access Denied");
     } else {
-        MemBuf *content = BuildContent();
-        rep->setHeaders(httpStatus, NULL, "text/html;charset=utf-8", content->contentSize(), 0, -1);
+        const auto body = buildBody();
+        rep->setHeaders(httpStatus, nullptr, "text/html;charset=utf-8", body.length(), 0, -1);
         /*
          * include some information for downstream caches. Implicit
          * replaceable content. This isn't quite sufficient. xerrno is not
@@ -1190,7 +1315,7 @@ ErrorState::BuildHttpReply()
          * might want to know. Someone _will_ want to know OTOH, the first
          * X-CACHE-MISS entry should tell us who.
          */
-        httpHeaderPutStrf(&rep->header, HDR_X_SQUID_ERROR, "%s %d", name, xerrno);
+        httpHeaderPutStrf(&rep->header, Http::HdrType::X_SQUID_ERROR, "%s %d", name, xerrno);
 
 #if USE_ERR_LOCALES
         /*
@@ -1201,55 +1326,43 @@ ErrorState::BuildHttpReply()
          */
         if (!Config.errorDirectory) {
             /* We 'negotiated' this ONLY from the Accept-Language. */
-            rep->header.delById(HDR_VARY);
-            rep->header.putStr(HDR_VARY, "Accept-Language");
+            static const SBuf acceptLanguage("Accept-Language");
+            rep->header.updateOrAddStr(Http::HdrType::VARY, acceptLanguage);
         }
 
         /* add the Content-Language header according to RFC section 14.12 */
         if (err_language) {
-            rep->header.putStr(HDR_CONTENT_LANGUAGE, err_language);
+            rep->header.putStr(Http::HdrType::CONTENT_LANGUAGE, err_language);
         } else
 #endif /* USE_ERROR_LOCALES */
         {
             /* default templates are in English */
             /* language is known unless error_directory override used */
             if (!Config.errorDirectory)
-                rep->header.putStr(HDR_CONTENT_LANGUAGE, "en");
+                rep->header.putStr(Http::HdrType::CONTENT_LANGUAGE, "en");
         }
 
-        rep->body.setMb(content);
-        /* do not memBufClean() or delete the content, it was absorbed by httpBody */
+        rep->body.set(body);
     }
 
     // Make sure error codes get back to the client side for logging and
     // error tracking.
     if (request) {
-        int edc = ERR_DETAIL_NONE; // error detail code
-#if USE_OPENSSL
         if (detail)
-            edc = detail->errorNo();
+            request->detailError(type, detail);
         else
-#endif
-            if (detailCode)
-                edc = detailCode;
-            else
-                edc = xerrno;
-        request->detailError(type, edc);
+            request->detailError(type, SysErrorDetail::NewIfAny(xerrno));
     }
 
     return rep;
 }
 
-MemBuf *
-ErrorState::BuildContent()
+SBuf
+ErrorState::buildBody()
 {
-    const char *m = NULL;
-
     assert(page_id > ERR_NONE && page_id < error_page_count);
 
 #if USE_ERR_LOCALES
-    ErrorPageFile *localeTmpl = NULL;
-
     /** error_directory option in squid.conf overrides translations.
      * Custom errors are always found either in error_directory or the templates directory.
      * Otherwise locate the Accept-Language header
@@ -1258,11 +1371,12 @@ ErrorState::BuildContent()
         if (err_language && err_language != Config.errorDefaultLanguage)
             safe_free(err_language);
 
-        localeTmpl = new ErrorPageFile(err_type_str[page_id], static_cast<err_type>(page_id));
-        if (localeTmpl->loadFor(request)) {
-            m = localeTmpl->text();
-            assert(localeTmpl->language());
-            err_language = xstrdup(localeTmpl->language());
+        ErrorPageFile localeTmpl(err_type_str[page_id], static_cast<err_type>(page_id));
+        if (localeTmpl.loadFor(request.getRaw())) {
+            inputLocation = localeTmpl.filename;
+            assert(localeTmpl.language());
+            err_language = xstrdup(localeTmpl.language());
+            return compileBody(localeTmpl.text(), true);
         }
     }
 #endif /* USE_ERR_LOCALES */
@@ -1271,44 +1385,152 @@ ErrorState::BuildContent()
      * If client-specific error templates are not enabled or available.
      * fall back to the old style squid.conf settings.
      */
-    if (!m) {
-        m = error_text[page_id];
 #if USE_ERR_LOCALES
-        if (!Config.errorDirectory)
-            err_language = Config.errorDefaultLanguage;
+    if (!Config.errorDirectory)
+        err_language = Config.errorDefaultLanguage;
 #endif
-        debugs(4, 2, HERE << "No existing error page language negotiated for " << errorPageName(page_id) << ". Using default error file.");
-    }
+    debugs(4, 2, "No existing error page language negotiated for " << this << ". Using default error file.");
+    return compileBody(error_text[page_id], true);
+}
 
-    MemBuf *result = ConvertText(m, true);
-#if USE_ERR_LOCALES
-    if (localeTmpl)
-        delete localeTmpl;
-#endif
-    return result;
+SBuf
+ErrorState::compileBody(const char *input, bool allowRecursion)
+{
+    return compile(input, false, allowRecursion);
 }
 
-MemBuf *ErrorState::ConvertText(const char *text, bool allowRecursion)
+SBuf
+ErrorState::compile(const char *input, bool building_deny_info_url, bool allowRecursion)
 {
-    MemBuf *content = new MemBuf;
-    const char *p;
-    const char *m = text;
-    assert(m);
-    content->init();
-
-    while ((p = strchr(m, '%'))) {
-        content->append(m, p - m);     /* copy */
-        const char *t = Convert(*++p, false, allowRecursion);  /* convert */
-        content->Printf("%s", t);      /* copy */
-        m = p + 1;                     /* advance */
+    assert(input);
+
+    Build build;
+    build.building_deny_info_url = building_deny_info_url;
+    build.allowRecursion = allowRecursion;
+    build.input = input;
+
+    auto blockStart = build.input;
+    while (const auto letter = *build.input) {
+        if (letter == '%') {
+            build.output.append(blockStart, build.input - blockStart);
+            compileLegacyCode(build);
+            blockStart = build.input;
+        }
+        else if (letter == '@' && LogformatMagic.cmp(build.input, LogformatMagic.length()) == 0) {
+            build.output.append(blockStart, build.input - blockStart);
+            compileLogformatCode(build);
+            blockStart = build.input;
+        } else {
+            ++build.input;
+        }
     }
+    build.output.append(blockStart, build.input - blockStart);
+    return build.output;
+}
 
-    if (*m)
-        content->Printf("%s", m);      /* copy tail */
+/// react to a compile() error
+/// \param msg  description of what went wrong
+/// \param near  approximate start of the problematic input
+/// \param  forceBypass whether detection of this error was introduced late,
+/// after old configurations containing this error could have been
+/// successfully validated and deployed (i.e. the admin may not be
+/// able to fix this newly detected but old problem quickly)
+void
+ErrorState::noteBuildError_(const char *msg, const char *near, const bool forceBypass)
+{
+    using ErrorPage::BuildErrorPrinter;
+    const auto runtime = !starting_up;
+    if (runtime || forceBypass) {
+        // swallow this problem because the admin may not be (and/or the page
+        // building code is not) ready to handle throwing consequences
+
+        static unsigned int seenErrors = 0;
+        ++seenErrors;
+
+        const auto debugLevel =
+            (seenErrors > 100) ? DBG_DATA:
+            (starting_up || reconfiguring) ? DBG_CRITICAL:
+            3; // most other errors have been reported as configuration errors
+
+        // Error fatality depends on the error context: Reconfiguration errors
+        // are, like startup ones, DBG_CRITICAL but will never become FATAL.
+        if (starting_up && seenErrors <= 10)
+            debugs(4, debugLevel, "WARNING: The following configuration error will be fatal in future Squid versions");
+
+        debugs(4, debugLevel, "ERROR: " << BuildErrorPrinter(inputLocation, page_id, msg, near));
+    } else {
+        throw TexcHere(ToSBuf(BuildErrorPrinter(inputLocation, page_id, msg, near)));
+    }
+}
+
+/* ErrorPage::BuildErrorPrinter */
+
+std::ostream &
+ErrorPage::BuildErrorPrinter::printLocation(std::ostream &os) const {
+    if (!inputLocation.isEmpty())
+        return os << inputLocation;
+
+    if (page_id < ERR_NONE || page_id >= error_page_count)
+        return os << "[error page " << page_id << "]"; // should not happen
 
-    content->terminate();
+    if (page_id < ERR_MAX)
+        return os << err_type_str[page_id];
+
+    return os << "deny_info " << ErrorDynamicPages.at(page_id - ERR_MAX)->page_name;
+}
+
+std::ostream &
+ErrorPage::BuildErrorPrinter::print(std::ostream &os) const {
+    printLocation(os) << ": " << msg << " near ";
+
+    // TODO: Add support for prefix printing to Raw
+    const size_t maxContextLength = 15; // plus "..."
+    if (strlen(near) > maxContextLength) {
+        os.write(near, maxContextLength);
+        os << "...";
+    } else {
+        os << near;
+    }
 
-    assert((size_t)content->contentSize() == strlen(content->content()));
+    // XXX: We should not be converting (inner) exception to text if we are
+    // going to throw again. See "add arbitrary (re)thrower-supplied details"
+    // TODO in TextException.h for a long-term in-catcher solution.
+    if (std::current_exception())
+        os << "\n    additional info: " << CurrentException;
 
-    return content;
+    return os;
 }
+
+/// add error page template to the global index
+static void
+ErrorPage::ImportStaticErrorText(const int page_id, const char *text, const SBuf &inputLocation)
+{
+    assert(!error_text[page_id]);
+    error_text[page_id] = xstrdup(text);
+    ValidateStaticError(page_id, inputLocation);
+}
+
+/// validate static error page
+static void
+ErrorPage::ValidateStaticError(const int page_id, const SBuf &inputLocation)
+{
+    // Supplying nil ALE pointer limits validation to logformat %code
+    // recognition by Format::Token::parse(). This is probably desirable
+    // because actual %code assembly is slow and should not affect validation
+    // when our ALE cannot have any real data (this code is not associated
+    // with any real transaction).
+    ErrorState anErr(err_type(page_id), Http::scNone, nullptr, nullptr);
+    anErr.inputLocation = inputLocation;
+    anErr.validate();
+}
+
+std::ostream &
+operator <<(std::ostream &os, const ErrorState *err)
+{
+    if (err)
+        os << errorPageName(err->page_id);
+    else
+        os << "[none]";
+    return os;
+}
+