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/hgText/hgText.c src/hg/hgText/hgText.c
index a858e475c17..91043c78128 100644
--- src/hg/hgText/hgText.c
+++ src/hg/hgText/hgText.c
@@ -3111,31 +3111,31 @@
 cgiMakeButton("phase", outputOptionsPhase);
 printf("</FORM>\n");
 webEnd();
 }
 
 
 void doTabSeparatedCT(boolean allFields)
 {
 struct bed *bedList;
 char *table = getTableName();
 struct customTrack *ct = lookupCt(table);
 struct slName *chosenFields;
 struct bedFilter *bf;
 boolean gotResults;
 
-printf("Content-Type: text/plain\n\n");
+cgiPrintContentType("text/plain");
 webStartText();
 checkTableExists(fullTableName);
 bf = constrainBedFields(NULL);
 
 chosenFields = getChosenFields(allFields);
 if (chosenFields == NULL)
     {
     printf("\n# Error: at least one field must be selected.\n\n");
     return;
     }
 
 if (allGenome)
     bedList = cloneBedList(ct->bedList);
 else
     bedList = bedFilterListInRange(ct->bedList, bf, chrom, winStart, winEnd);
@@ -3160,31 +3160,31 @@
 char *constraints;
 boolean gotResults;
 
 checkUserKeys();
 saveChooseTableState();
 saveChooseFieldsState();
 saveOutputOptionsState();
 saveIntersectOptionsState();
 
 if (sameString(customTrackPseudoDb, db))
     {
     doTabSeparatedCT(allFields);
     return;
     }
 
-printf("Content-Type: text/plain\n\n");
+cgiPrintContentType("text/plain");
 webStartText();
 checkTableExists(fullTableName);
 hti = getHti(db, table);
 constraints = constrainFields(NULL);
 
 if (allGenome)
     chromList = hAllChromNames();
 else
     chromList = newSlName(chrom);
 
 dyStringClear(fieldSpec);
 if (allFields)
     dyStringAppend(fieldSpec, "*");
 else
     {
@@ -3355,31 +3355,31 @@
 webEnd();
 }
 
 
 void doGetSequence()
 /* Display FASTA sequence. */
 {
 struct hTableInfo *hti = getOutputHti();
 struct bed *bedList;
 int itemCount;
 
 saveOutputOptionsState();
 saveIntersectOptionsState();
 saveSequenceOptionsState();
 
-printf("Content-Type: text/plain\n\n");
+cgiPrintContentType("text/plain");
 webStartText();
 bedList = getBedList(FALSE, NULL);
 
 itemCount = hgSeqBedDb(database, hti, bedList);
 bedFreeList(&bedList);
 if (itemCount == 0)
     printf("\n# No results returned from query.\n\n");
 }
 
 static void addGffLineFromBed(struct gffLine **pGffList, struct bed *bed,
 			      char *source, char *feature,
 			      int start, int end, char frame, char *txName)
 /* Create a gffLine from a bed and line-specific parameters, add to list. */
 {
 struct gffLine *gff;
@@ -3604,31 +3604,31 @@
 struct hTableInfo *hti = getOutputHti();
 struct bed *bedList;
 struct gffLine *gffList, *gffPtr;
 char source[64];
 char *db = getTableDb();
 char *track = getTrackName();
 int itemCount;
 // Would be nice to allow user to select this, but I don't want to
 // make an options page for just one param... any others?
 // ? exon / CDS ?
 boolean gtf2StopCodons = FALSE;
 
 saveOutputOptionsState();
 saveIntersectOptionsState();
 
-printf("Content-Type: text/plain\n\n");
+cgiPrintContentType("text/plain");
 webStartText();
 bedList = getBedList(FALSE, NULL);
 
 if (sameString(customTrackPseudoDb, db))
     snprintf(source, sizeof(source), "%s", track);
 else
     snprintf(source, sizeof(source), "%s_%s", db, track);
 itemCount = 0;
 gffList = bedToGffLines(bedList, hti, source, gtf2StopCodons);
 bedFreeList(&bedList);
 for (gffPtr = gffList;  gffPtr != NULL;  gffPtr = gffPtr->next)
     {
     gffTabOut(gffPtr, stdout);
     itemCount++;
     }
@@ -3767,31 +3767,31 @@
 char *ctName = cgiUsualString("tbCtName", table);
 char *ctDesc = cgiUsualString("tbCtDesc", table);
 char *ctVis  = cgiUsualString("tbCtVis", "dense");
 char *ctUrl  = cgiUsualString("tbCtUrl", "");
 char *fbQual = fbOptionsToQualifier();
 char fbTQ[128];
 int fields;
 boolean gotResults = FALSE;
 
 saveOutputOptionsState();
 saveIntersectOptionsState();
 saveBedCtOptionsState();
 
 if (! doCt)
     {
-    printf("Content-Type: text/plain\n\n");
+    cgiPrintContentType("text/plain");
     webStartText();
     }
 
 bedList = getBedList(FALSE, NULL);
 
 fields = hTableInfoBedFieldCount(hti);
 
 if (doCtHdr && (bedList != NULL))
     {
     int visNum = (int) hTvFromStringNoAbort(ctVis);
     if (visNum < 0)
 	visNum = 0;
     if (doCt)
 	ctNew = newCT(ctName, ctDesc, visNum, ctUrl, fields);
     else