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/hgSession/backup.c src/hg/hgSession/backup.c
index f484092aaa7..ef103f13ce5 100644
--- src/hg/hgSession/backup.c
+++ src/hg/hgSession/backup.c
@@ -45,31 +45,31 @@
     htmlVaWarn("Region selected is too large for calculation. Please specify a smaller region or try limiting to fewer data points.", args);
 else
     {
     // call previous handler
     popWarnHandler();
     vaWarn(format, args);
     }
 if(isErrAbortInProgress())
     noWarnAbort();
 }
 
 static void vaHtmlOpen(char *format, va_list args)
 /* Start up a page that will be in html format. */
 {
 cspWriteResponseHeader();
-puts("Content-Type:text/html\n");
+cgiPrintContentType("text/html");
 cartVaWebStart(cart, database, format, args);
 pushWarnHandler(errAbortHandler);
 }
 
 void htmlOpen(char *format, ...)
 /* Start up a page that will be in html format. */
 {
 va_list args;
 va_start(args, format);
 vaHtmlOpen(format, args);
 va_end(args);
 }
 
 void htmlClose()
 /* Close down html format page. */
@@ -167,31 +167,31 @@
     errAbort("No complete html found");
     htmlClose();
     return;
     }
 int start;
 for (start=end; start >= 0 && ! (startsWith("<html>", lines[start]) || startsWith("<HTML>", lines[start])) ; --start)
     /* do nothing */ ;
 if (start < 0)
     {
     htmlOpen("Background Status");
     errAbort("No html start tag found");
     htmlClose();
     return;
     }
 cspWriteResponseHeader();
-puts("Content-Type: text/html\n");
+cgiPrintContentType("text/html");
 int line;
 boolean autoRefreshFound = FALSE;
 boolean successfullyUploaded = FALSE;
 for (line=start; line <= end; line++)
     {
     puts(lines[line]);
     if (startsWith("setTimeout(function(){location = location;}", lines[line]))
 	autoRefreshFound = TRUE;
     if (startsWith("Output has been successfully uploaded", lines[line]))
 	successfullyUploaded = TRUE;
     }
 // if it looks like the background is no longer running, 
 // include the .err stdout output for more informative problem message
 char urlErr[512];
 char *textErr = NULL;
@@ -708,31 +708,31 @@
 
 printf("<br>\n");
 printf("<br>\n");
 
 }
 
 
 void showDownloadSessionCtData(struct hashEl *downloadList)
 /* Show download page for the given session */
 {
 char query[512];
 char **row = NULL;
 struct sqlResult *sr = NULL;
 
 cspWriteResponseHeader();
-puts("Content-Type:text/html\n");
+cgiPrintContentType("text/html");
 cartWebStart(cart, NULL, "Backup Custom Tracks");
 jsInit();
 
 struct sqlConnection *conn = hConnectCentral();
 
 printf("<FORM ACTION=\"%s\" NAME=\"mainForm\" METHOD=POST "
        "ENCTYPE=\"multipart/form-data\">\n",
        hgSessionName());
 cartSaveSession(cart);
 
 
 sqlSafef(query, sizeof(query), "SELECT firstUse, contents from %s "
     "WHERE id=%lu",
     "sessionDb", cartSessionRawId(cart));
 
@@ -1230,34 +1230,33 @@
 struct hashEl *hel = NULL;
 
 // I think it should only get one at a time, not a list.
 hel = downloadPathList;
 
 char *encDownPath = hel->name + strlen(hgsDoDownloadPrefix);
 char *downPath = cgiDecodeClone(encDownPath);
 
 char *fileName = cartString(cart, hgsSaveLocalBackupFileName);
 
 char outFile[1024];
 safef(outFile, sizeof outFile, "%s.tar.gz", fileName);
 
 long fSize = fileSize(downPath);
 
-printf("Content-Type: application/octet-stream\n");
 printf("Content-Disposition: attachment; filename=\"%s\"\n", outFile);
 printf("Content-Length: %ld\n", fSize);
-printf("\n");
+cgiPrintContentType("application/octet-stream");
 
 FILE *f = mustOpen(downPath, "r");
 long remaining = fSize;
 int bufSize = 65536;
 char *buf = needMem(bufSize);
 while (remaining)
     {
     int bufRemain = bufSize;
     if (bufRemain > remaining)
 	bufRemain = remaining;
     mustRead(f, buf, bufRemain); // mustRead OK since we are reading from disk
     mustWrite(stdout, buf, bufRemain);
     remaining -= bufRemain;
     lazarusLives(20 * 60);   // extend keep-alive time. for big downloads on slow connections.
     }