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/hgTracks/hgTracks.c src/hg/hgTracks/hgTracks.c
index 09bf1f9faa7..9c1893a16d8 100644
--- src/hg/hgTracks/hgTracks.c
+++ src/hg/hgTracks/hgTracks.c
@@ -6137,63 +6137,66 @@
 hvGfxClose(&hvg);
 if (measureTiming)
     measureTime("Time completed writing trash hgt png image file");
 
 #ifdef SUPPORT_CONTENT_TYPE
 char *type = cartUsualString(cart, "hgt.contentType", "html");
 if(sameString(type, "jsonp"))
     {
     struct jsonElement *json = newJsonObject(newHash(8));
     char *jsonp = cartString(cart, "jsonp");
     // This path only ever emits a wrapped response, so there is no bare form to
     // fall back to: reject an invalid callback name outright.
     if (!isValidJsonpCallback(jsonp))
         errAbort("invalid callback");
 
-    printf("Content-Type: application/json\n\n");
+    cgiPrintContentType("application/json");
     errAbortSetDoContentType(FALSE);
     jsonObjectAdd(json, "track", newJsonString(cartString(cart, "hgt.trackNameFilter")));
     jsonObjectAdd(json, "height", newJsonNumber(pixHeight));
     jsonObjectAdd(json, "width", newJsonNumber(pixWidth));
     jsonObjectAdd(json, "img", newJsonString(pngTn.forHtml));
     printf("%s(", jsonp);
     hPrintEnable();
     jsonPrint((struct jsonElement *) json, NULL, 0);
     hPrintDisable();
     printf(")\n");
     return;
     }
 else if(sameString(type, "png") || sameString(type, "pdf") || sameString(type, "eps"))
     {
     // following code bypasses html and return png's directly - see redmine 4888
     // NB: Pretty sure the pdf and eps options here are never invoked.  I don't see any
     // calls that would activate eps output, and pdf is locked behind an unused ifdef
     char *file;
     if(sameString(type, "pdf"))
         {
-        printf("Content-Disposition: filename=hgTracks.pdf\nContent-Type: application/pdf\n\n");
+        printf("Content-Disposition: filename=hgTracks.pdf\n");
+        cgiPrintContentType("application/pdf");
         file = convertEpsToPdf(psOutput);
         unlink(psOutput);
         }
     else if(sameString(type, "eps"))
         {
-        printf("Content-Disposition: filename=hgTracks.eps\nContent-Type: application/eps\n\n");
+        printf("Content-Disposition: filename=hgTracks.eps\n");
+        cgiPrintContentType("application/eps");
         file = psOutput;
         }
     else
         {
-        printf("Content-Disposition: filename=hgTracks.png\nContent-Type: image/png\n\n");
+        printf("Content-Disposition: filename=hgTracks.png\n");
+        cgiPrintContentType("image/png");
         file = pngTn.forCgi;
         }
 
     char buf[4096];
     FILE *fd = fopen(file, "r");
     if(fd == NULL)
         // fail some other way (e.g. HTTP 500)?
         errAbort("Couldn't open png for reading");
     while (TRUE)
         {
         size_t n = fread(buf, 1, sizeof(buf), fd);
         if(n)
             fwrite(buf, 1, n, stdout);
         else
             break;
@@ -9306,31 +9309,31 @@
         if (hTrackOnChrom(track->tdb, w->chromName))
             hideIt = FALSE;
         }
     if (hideIt)
         {
         track->limitedVis = tvHide;
         track->limitedVisSet = TRUE;
         }
     }
 
 if (cartUsualBoolean(cart, "dumpTracks", FALSE))
     {
     struct dyString *dy = dyStringNew(1024);
     logTrackList(dy, trackList);
 
-    printf("Content-type: text/html\n\n");
+    cgiPrintContentType("text/html");
     printf("%s\n", dy->string);
     exit(0);
     }
 
 if (sameString(cfgOptionDefault("trackLog", "off"), "on"))
     logTrackVisibilities(cartSessionId(cart), trackList, position);
 
 struct track *visibleTracks = getVisibleTracks(trackList);
 if (visibleTracks)
     {
     // add these tracks to the special 'visible' group
     struct group *group, *visibleGroup = NULL;
     for (group = groupList; group != NULL; group = group->next)
         {
         if (sameString(group->name, "visible"))