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/lib/cheapcgi.c src/lib/cheapcgi.c
index 715d16f803a..eb8f48a7e38 100644
--- src/lib/cheapcgi.c
+++ src/lib/cheapcgi.c
@@ -320,36 +320,65 @@
 
 void dumpCookieList()
 /* Print out the cookie list. */
 {
 struct cgiVar *v;
 for (v=cookieList; v != NULL; v = v->next)
     printf("%s=%s (%d)\n", v->name, v->val, v->saved);
 }
 
 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. */
+{
+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: 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.  A second header cannot
+ * reach the browser as a header - it lands in the page body as text - so a
+ * later caller is always the mistaken one, and several flows (hgc emitting the
+ * header early, then webStart asking again) reach here twice by design. */
 {
+if (didContentType)
+    return;
+didContentType = TRUE;
+struct slPair *h;
+for (h = cgiExtraHeaders; h != NULL; h = h->next)
+    printf("%s: %s\n", h->name, (char *)h->val);
 if (contentType == NULL)
     contentType = "text/html";
 printf("Content-Type: %s\n\n", contentType);
 }
 
 boolean cgiIsOnWeb()
 /* Return TRUE if looks like we're being run as a CGI. 
  * You cannot use this in your own CGIs to determine if you're run from the command line, 
  * as cgiFromCommandLine() will set this parameter.*/
 {
 return getenv("REQUEST_METHOD") != NULL;
 }
 
 
 char *cgiRequestMethod()