cb99f0b11bdeee5dfa76064d38b6410da0f4a709
max
  Thu Sep 10 00:55:21 2026 -0700
Centralize CGI Content-Type printing in one cgiPrintContentType() helper

Around 90 places across the tree hand-rolled the CGI response header, each
with its own spelling: "Content-Type:" or "Content-type:", \n or \r\n, and
the terminating blank line written as part of the same string, as a separate
puts("\n") (which emits two newlines, so a stray blank line led the body) or
as printf("\r\n\r\n") (two blank lines).  A handful forgot the terminator
entirely and relied on a following header to supply it.

cgiPrintContentType() in lib/cheapcgi.c now writes the Content-Type line and
the blank line that ends the header.  Header lines are not ordered, so the
callers that also send Status, Set-Cookie, Content-Disposition, Content-Length
or X-Sendfile write those first and call this last to close the header; that
keeps it to a single helper rather than a print-the-line / end-the-header pair
that a caller can half-use.  cart.c's existing httpHeaders list already worked
this way.

Only the CGI response path is touched.  The dyStringPrintf("Content-type: ...")
calls that build outgoing HTTP *requests* (genomeSpace, oauthLogin, eapMetaSync,
edwWebAuthLogin, ga4ghToBed) are unrelated and left alone.

Also fills out the apiKey error message in botDelay.c to say where to create a
key and that keys are server-specific.

No behavior change on the wire beyond dropping those stray blank lines and
adding the missing newline after Retry-After.

diff --git src/hg/hgTrackUi/hgTrackUi.c src/hg/hgTrackUi/hgTrackUi.c
index c852a23a148..f38e4fb47ba 100644
--- src/hg/hgTrackUi/hgTrackUi.c
+++ src/hg/hgTrackUi/hgTrackUi.c
@@ -3855,44 +3855,47 @@
 
 // NOTE: Currently only composite multi-view tracks because
 // reset relies upon all cart vars following naming convention:
 //   {track}.{varName}...  ( One exception supported: {track}_sel ).
 
 if (trackDbLocalSetting(tdb, "container"))
     {
     /* For the moment, be a composite... */
     tdbMarkAsComposite(tdb);
     }
 if (ajax && cartOptionalString(cart, "descriptionOnly"))
     {
     char *liftDb = cloneString(trackDbSetting(tdb, "quickLiftDb"));
     if (liftDb)
         tdb->html = getTrackHtml(liftDb, tdb->table);
+    // resolve $hgsid, and for a hub the rest of its description page variables
+    hVarSubstTrackDbHtml(cart, tdb, database);
     //struct trackDb *tdbParent = tdbFillInAncestry(cartString(cart, "db"),tdb);
     if (tdb->html != NULL && tdb->html[0] != 0)
         {
         printRelatedTracks(database,trackHash,tdb,cart);
         puts(tdb->html);
         }
     else
         {
         struct trackDb *tdbParent = tdb->parent;
         for (;tdbParent && (tdbParent->html == NULL || tdbParent->html[0] == 0);
               tdbParent = tdbParent->parent )
             ; // Get the first parent that has html
         if (tdbParent != NULL && tdbParent->html != NULL && tdbParent->html[0])
             {
+            hVarSubstTrackDbHtml(cart, tdbParent, database);
             printf("<h2 style='color:%s'>Retrieved from %s Track...</h2>\n",
                    COLOR_DARKGREEN,tdbParent->shortLabel);
             printRelatedTracks(database,trackHash,tdb,cart);
             puts(tdbParent->html);
             }
         else
             printf("<h2>No description found for: %s.</h2>",tdbParent?tdbParent->track:tdb->track);
         }
     cartRemove(cart,"descriptionOnly"); // This is a once only request and should be deleted
     return;
     }
 if (tdbIsContainer(tdb) || tdbIsSuperTrack(tdb))
     {
     safef(setting,sizeof(setting),"%s.%s",tdb->track,RESET_TO_DEFAULTS);
     // NOTE: if you want track vis to not be reset, move to after vis dropdown
@@ -4285,30 +4288,32 @@
 
     char *genome = hGenome(database);
     char *desc = hFreezeDateOpt(database);
     printf("<b>Assembly:</b> %s %s<br>", genome, desc);
 
     /* Print lift information from trackDb, if any */
     trackDbPrintOrigAssembly(tdb, database);
 
     printUpdateTime(database, tdb, NULL);
     }
 
 char *liftDb = cloneString(trackDbSetting(tdb, "quickLiftDb"));
 // quickLiftChain has static html
 if (liftDb && differentString(trackHubSkipHubName(tdb->track), "quickLiftChain"))
     tdb->html = getTrackHtml(liftDb, tdb->table);
+// resolve $hgsid, and for a hub the rest of its description page variables
+hVarSubstTrackDbHtml(cart, tdb, database);
 if (tdb->html != NULL && tdb->html[0] != 0)
     {
     char *browserVersion;
     if (btIE == cgiClientBrowser(&browserVersion, NULL, NULL) && *browserVersion < '8')
         htmlHorizontalLine();
     else // Move line down, since <H2>Description (in ->html) is proceded by too much space
         printf("<HR ALIGN='bottom' style='position:relative; top:1em;'>");
 
     printf("<table class='windowSize'><tr valign='top'><td rowspan=2>");
     puts("<A NAME='TRACK_HTML'></A>");    // include anchor for Description link
 
     // Add pennantIcon
     printPennantIconNote(tdb);
 
     char *html = tdb->html;
@@ -4487,60 +4492,60 @@
     puts("Status: 400 Bad Request");
     errAbort("Supplied fileUrl does not match any connected hubs or track settings.");
     }
 
 // By now we know that fileUrl points to something valid to fetch and return to the user.
 // Now we just have to fetch the file contents and retransmit it.
 
 int timeout = cartUsualInt(cart, "udcTimeout", 300);
 if (udcCacheTimeout() < timeout)
     udcSetCacheTimeout(timeout);
 
 struct udcFile *udc = udcFileMayOpen(fileUrl, NULL);
 if (udc == NULL)
     {
     puts("Status: 404 Not Found");
-    puts("Content-Type: text/plain\n");
+    cgiPrintContentType("text/plain");
     printf("Error: could not open %s\n", fileUrl);
     freeMem(fileUrl);
     return;
     }
 
 char maxAge[1024];
 safef(maxAge, sizeof(maxAge), "max-age=%d", timeout);
 printf("Cache-Control: %s\n", maxAge);
 
 // See if we're getting a "has it changed" request.
 // If so, return a 304 if nothing changed.
 char etag[1024];
 time_t mtime = udcUpdateTime(udc);
 safef(etag, sizeof(etag), "\"%ld\"", mtime);
 printf("ETag: %s\n", etag);
 udcFileClose(&udc);
 
 char *ifNone = getenv("HTTP_IF_NONE_MATCH");
 if (isNotEmpty(ifNone))
     {
     if (sameStringN(etag, ifNone, strlen(etag)-1)) // Apache can add -gzip to etags during transmission
         {
         puts("Status: 304 Not Modified\n");
         freeMem(fileUrl);
         return;
         }
     }
 
-puts("Content-Type: text/plain\n");
+cgiPrintContentType("text/plain");
 char *content = udcFileReadAll(fileUrl, NULL, 0, NULL);
 puts(content);
 freeMem(content);
 freeMem(fileUrl);
 }
 
 void doMiddle(struct cart *theCart)
 /* Write body of web page. */
 {
 boolean isFileFetch = isNotEmpty(cartOptionalString(theCart, "fileUrl"));
 
 if (isFileFetch)
     {
     handleFileFetch(theCart);  // file fetch workaround for CORS issues
     return;