998149024f8bde04e8b76638043cfeac3d158731
max
  Tue Sep 15 05:47:27 2026 -0700
Queue the cart cookie and the content policy instead of printing them, refs #38353

cartWriteHeaderAndCont() guards on cgiDidContentType(), which any cgiPrintContentType()
anywhere sets.  An early warn() during cartNew -- "Unable to load session file" reaches the
early warning handler, which calls htmlStart -- prints the header before there is a cart,
and from then on the Set-Cookie and Content-Security-Policy lines were silently skipped.
Nothing changed on the wire, since before the guard they landed in the page body as text
and were equally inert, but skipping them silently is not the behaviour to keep.

cartWriteCookie() and cspWriteResponseHeader() now hand their lines to cgiAddHttpHeader(),
so whichever call prints the content type prints them too and the order of the calls no
longer matters.  cartAndCookieWithHtml() queues the policy before the early handlers are
pushed, so even a page written by that early warn carries one.  The cookie cannot be queued
that early -- there is no cart yet -- and is still lost on that path; the comment says so.

cgiAddHttpHeader() now does what its own comment already promised and ignores a header
added after the block was closed, rather than growing a list nothing will ever print.

getCspPolicyString() is declared in htmshell.h so hCommon.c can queue the value on its own.

diff --git src/lib/cheapcgi.c src/lib/cheapcgi.c
index 63c26ff7489..b739b4becfc 100644
--- src/lib/cheapcgi.c
+++ src/lib/cheapcgi.c
@@ -328,30 +328,32 @@
 
 void useTempFile()
 /* tell cheapcgi to use temp files */
 {
 doUseTempFile = TRUE;
 }
 
 static struct slPair *cgiExtraHeaders = NULL;   /* Written ahead of the content type. */
 static boolean didContentType = FALSE;          /* Has the response header been written? */
 
 void cgiAddHttpHeader(char *name, char *value)
 /* Add an HTTP header for cgiPrintContentType() to write ahead of the Content-Type
  * line, e.g. cgiAddHttpHeader("Cache-Control", "no-store").  Both strings are
  * cloned.  Has no effect once the header has been written. */
 {
+if (didContentType)
+    return;   // the header block is closed; nothing would ever print this
 slPairAdd(&cgiExtraHeaders, name, cloneString(value));
 }
 
 boolean cgiDidContentType()
 /* Return TRUE if the CGI response header has already been written. */
 {
 return didContentType;
 }
 
 void cgiPrintContentType(char *contentType)
 /* Write the CGI response header: any headers added with cgiAddHttpHeader(), a
  * Content-Type line, and the blank line that ends the header.  contentType NULL
  * means "text/html".  Header lines are not ordered, so a CGI that also sends
  * Status, Set-Cookie, Content-Disposition or the like writes those first and
  * calls this last to close the header.