cf9f4cb7f55c7beb8ad5f11118656a60770a71a5
max
  Thu Sep 10 01:02:36 2026 -0700
Move the extra-HTTP-header list into cheapcgi, and write the header only once

Follow-on to the cgiPrintContentType() refactor.

cart.c owned the mechanism for adding headers ahead of the content type: a
global slPair list plus addHttpHeaders() to print it.  That put it in hg/lib,
out of reach of the CGIs and library code that do not use a cart, even though
nothing about it is cart-specific.  It now lives next to cgiPrintContentType()
in lib/cheapcgi.c, behind cgiAddHttpHeader(name, value) instead of a bare
global, and cgiPrintContentType() writes the queued headers itself.  The one
caller, hgTracks/mainMain.c, reads the same but no longer reaches into cart.h
for it.  cspWriteResponseHeader() stays in hg/lib where it belongs, since it
needs hg.conf; cartWriteHeaderAndCont() calls it directly now, the way the
other ten callers already do.

cgiPrintContentType() also writes at most once per process now.  A second
content type cannot reach the browser as a header - it lands in the page body
as text - so the later caller is always the mistaken one.  cart.c had a private
cartDidContentType flag for exactly this, covering only the flows that went
through the cart; the guard is now in the one function every flow shares, and
cartDidContentType is gone.  Its public equivalent, cgiDidContentType(), is
what cartWriteHeaderAndCont() checks so it does not write a second cookie.

Verified: make libs, make cgi and the lib test suite are clean, hgTracks still
emits Cache-Control: no-store, and hgTracks, hgc and hgTables each emit exactly
one Content-Type on both their html and their text paths.

diff --git src/inc/cheapcgi.h src/inc/cheapcgi.h
index a2400f88e4c..8c49a65ff8d 100644
--- src/inc/cheapcgi.h
+++ src/inc/cheapcgi.h
@@ -112,35 +112,45 @@
 /* Free up resources associated with dictionary. */
 
 void cgiDictionaryFreeList(struct cgiDictionary **pList);
 /* Free up a whole list of cgiDictionaries */
 
 struct cgiDictionary *cgiDictionaryFromEncodedString(char *encodedString);
 /* Giving a this=that&this=that string,  return cgiDictionary parsed out from it. 
  * This does *not* destroy input like the lower level cgiParse functions do. */
 
 char *findCookieData(char *varName);
 /* Get the string associated with varName from the cookie string. */
 
 void dumpCookieList();
 /* Print out the cookie list. */
 
+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. */
+
+boolean cgiDidContentType();
+/* Return TRUE if the CGI response header has already been written. */
+
 void cgiPrintContentType(char *contentType);
-/* Write the CGI response header: 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. */
+/* 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.  Only the first call in a process writes
+ * anything. */
 
 boolean cgiIsOnWeb();
 /* Return TRUE if looks like we're being run as a CGI. */
 
 char *cgiRequestMethod();
 /* Return CGI REQUEST_METHOD (such as 'GET/POST/PUT/DELETE/HEAD') */
 
 char *cgiRequestUri();
 /* Return CGI REQUEST_URI */
 
 char *cgiRequestContentLength();
 /* Return HTTP REQUEST CONTENT_LENGTH if available*/
 
 char *cgiScriptName();
 /* Return name of script so libs can do context-sensitive stuff. */