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/hg/lib/cart.c src/hg/lib/cart.c
index 1af1867ceef..f631631b6a1 100644
--- src/hg/lib/cart.c
+++ src/hg/lib/cart.c
@@ -39,33 +39,30 @@
 #include "quickLift.h"
 #include "pcrResult.h"
 #include "botDelay.h"
 #include "curlWrap.h"
 #include "hubSpaceKeys.h"
 #include "myVariantsShare.h"
 #include "customTrack.h"
 #include "dupTrack.h"
 #include "myVariants.h"
 
 static char *sessionVar = "hgsid";	/* Name of cgi variable session is stored in. */
 static char *positionCgiName = "position";
 
 DbConnector cartDefaultConnector = hConnectCart;
 DbDisconnect cartDefaultDisconnector = hDisconnectCart;
-static boolean cartDidContentType = FALSE;
-
-struct slPair *httpHeaders = NULL; // A list of headers to output before the content-type
 
 static void hashUpdateDynamicVal(struct hash *hash, char *name, void *val)
 /* Val is a dynamically allocated (freeMem-able) entity to put
  * in hash.  Override existing hash item with that name if any.
  * Otherwise make new hash item. */
 {
 struct hashEl *hel = hashLookup(hash, name);
 if (hel == NULL)
     hashAdd(hash, name, val);
 else
     {
     freeMem(hel->val);
     hel->val = val;
     }
 }
@@ -3033,77 +3030,61 @@
 
 char *timeStr;
 if ( (timeStr = cgiOptionalString("_dumpCart")) != NULL)
     {
     dumpCartWithTime(cart, timeStr);
     exit(0);
     }
 
 // activate optional debuging output for CGIs
 verboseCgi(cgiUsualString("verbose", NULL));
 cartExclude(cart, "verbose");
 
 return cart;
 }
 
-static void addHttpHeaders()
-/* CGIs can initialize the global variable httpHeaders to control their own HTTP
- * headers. This allows, for example, to prevent web browser caching of hgTracks
- * responses, but implicitly allow web browser caching everywhere else */
-{
-struct slPair *h;
-for (h = httpHeaders; h != NULL; h = h->next)
-    {
-    printf("%s: %s\n", h->name, (char *)h->val);
-    }
-cspWriteResponseHeader();
-}
-
 void cartWriteHeaderAndCont(struct cart* cart, char *cookieName, char *contType)
 /* write http headers including cookie and content type line.
  * contType defaults to text/html when NULL.
  * cookieName defaults to hUserCookie() when NULL */
 {
-/* The CGI header must be written exactly once; a second write lands in the page body.  Some flows
- * (e.g. hgc) emit it early via cartAndCookieWithHtml before a later webStart also asks for it, so
- * guard here rather than trusting every caller to check cartDidContentType first. */
-if (cartDidContentType)
+/* cgiPrintContentType() writes the header only once per process, so the flows that reach here
+ * twice (e.g. hgc emitting it early via cartAndCookieWithHtml, then webStart asking again) do
+ * not need to check first.  Return early anyway, so we do not write a second cookie either. */
+if (cgiDidContentType())
     return;
-if (!contType)
-    contType = "text/html";
 if (!cookieName)
     cookieName = hUserCookie();
 
-addHttpHeaders();
+cspWriteResponseHeader();
 cartWriteCookie(cart, cookieName);
 cgiPrintContentType(contType);
-cartDidContentType = TRUE;
 }
 
 struct cart *cartAndCookieWithHtml(char *cookieName, char **exclude,
                                    struct hash *oldVars, boolean doContentType)
 /* Load cart from cookie and session cgi variable.  Write cookie
  * and optionally content-type part HTTP preamble to web page.  Don't
  * write any HTML though. */
 {
 // Note: early abort works fine but early warn does not
 htmlPushEarlyHandlers();
 struct cart *cart = cartForSession(cookieName, exclude, oldVars);
 popWarnHandler();
 popAbortHandler();
 
-if (doContentType && !cartDidContentType)
+if (doContentType)
     cartWriteHeaderAndCont(cart, cookieName, NULL);
 
 return cart;
 }
 
 struct cart *cartAndCookie(char *cookieName, char **exclude,
                            struct hash *oldVars)
 /* Load cart from cookie and session cgi variable.  Write cookie and
  * content-type part HTTP preamble to web page.  Don't write any HTML though. */
 {
 return cartAndCookieWithHtml(cookieName, exclude, oldVars, TRUE);
 }
 
 struct cart *cartAndCookieNoContent(char *cookieName, char **exclude,
                                     struct hash *oldVars)
@@ -3123,36 +3104,32 @@
 if (status == 0)
     {
     doMiddle(cart);
     }
 hDumpStackPopAbortHandler();
 popAbortHandler();
 }
 
 void cartEarlyWarningHandler(char *format, va_list args)
 /* Write an error message so user can see it before page is really started. */
 {
 static boolean initted = FALSE;
 va_list argscp;
 va_copy(argscp, args);
 if (!initted && !cgiOptionalString("ajax"))
-    {
-    if (!cartDidContentType)
     {
     cgiPrintContentType("text/html");
-        cartDidContentType = TRUE;
-        }
     htmStart(stdout, "Early Error");
     initted = TRUE;
     }
 printf("%s", htmlWarnStartPattern());
 htmlVaEncodeErrorText(format,args);
 printf("%s", htmlWarnEndPattern());
 
 /* write warning/error message to stderr so they get logged. */
 logCgiToStderr();
 vfprintf(stderr, format, argscp);
 va_end(argscp);
 putc('\n', stderr);
 fflush(stderr);
 }