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/lib/cart.c src/hg/lib/cart.c
index 0424156ec1f..1af1867ceef 100644
--- src/hg/lib/cart.c
+++ src/hg/lib/cart.c
@@ -1767,31 +1767,31 @@
     // A CGI run from the command line has no browser to solve a captcha, so the
     // challenge page would just replace the output the caller asked for. Only a
     // real command-line run reaches here with wasSpoofed set: cgiFromCommandLine()
     // returns early, leaving it FALSE, whenever the web server has set
     // REQUEST_METHOD, so this cannot be reached from an HTTP request.
     if (cgiWasSpoofed())
         return;
 
     char *cfSiteKey = cfgVal(CLOUDFLARESITEKEY);
     if (!cfSiteKey)
         return;
 
     if (cfgOptionBooleanDefault("captchaDebug", FALSE))
         fprintf(stderr, "CAPTCHA_PRINT %s\n", getSessionId());
     cspWriteResponseHeader();
-    puts("Content-Type:text/html\n"); // puts outputs one newline. Header requires two newlines.
+    cgiPrintContentType("text/html");
     puts("<html><head>");
     printf("<script nonce='%s'>\n", getNonce());
     printf("function showWidget() { \n"
        "turnstile.render('#myWidget', {\n"
          "sitekey: '%s',\n"
          "theme: 'light',\n"
          "callback: function (token) {\n"
          "     const parser = new URL(window.location);\n"
          "     parser.searchParams.set('token', token);\n"
          "     window.location = parser.href;\n"
          "   },\n"
        "});\n"
        "}\n", cfSiteKey);
     puts("</script>");
     puts("</head><body>");
@@ -1877,31 +1877,31 @@
                 {
                 struct sqlConnection *conn = hConnectCentralNoCache();
                 char *table = cfgOptionDefault("hguidIpTracking.table", "hguidIpAccess");
                 char query[256];
                 sqlSafef(query, sizeof(query),
                          "DELETE FROM %s WHERE userId=%lu", table, userIdNum);
                 sqlUpdate(conn, query);
                 sqlDisconnect(&conn);
                 }
             }
         return;
         }
     else
         {
         cspWriteResponseHeader();
-        puts("Content-Type: text/html\n");
+        cgiPrintContentType("text/html");
         puts("<html><body>Internal captcha error: Cloudflare rejected the captcha token. "
                 "Something is not working internally, we are very sorry. You can try reloading the page. "
                 "If this problem persists, send an email to genome-www@soe.ucsc.edu and we will "
                 "look into it as quickly as we can in the PST timezone. You can use any internet browser "
                 "where you have used the genome browser before, but not from this internet browser. "
                 "You can try our mirror sites, "
                 "genome-euro.ucsc.edu or genome-asia.ucsc.edu, while we are working on a solution.</body></html>");
         fprintf(stderr, "CAPTCHA_REJECT %s\n", getSessionId());
         exit(0);
         }
 }
 
 printCaptcha();
 }
 
@@ -2918,31 +2918,31 @@
         printf("Set-Cookie: redirect=%s; path=/; domain=%s; expires=%s\r\n", redirect, cgiServerName(), cookieDate());
         }
     }
 /* Validate login cookies if login is enabled */
 if (loginSystemEnabled())
     {
     struct slName *newCookies = loginValidateCookies(cart), *sl;
     for (sl = newCookies;  sl != NULL;  sl = sl->next)
         printf("Set-Cookie: %s\r\n", sl->name);
     }
 }
 
 static void cartJsonStart()
 /* Write the necessary headers for Apache */
 {
-puts("Content-Type: application/json\n");
+cgiPrintContentType("application/json");
 }
 
 static void cartJsonEnd(struct jsonWrite *jw)
 /* Write the final string which may have nothing in it */
 {
 if (jw)
     puts(jw->dy->string);
 }
 
 static void dumpCartWithTime(struct cart *cart, char *timeStr)
 /* Dump out the current cart to trash named by how long a page draw took using it. */
 {
 char prefix[128];
 
 // zero pad so the files will alphanumerically sort by elapsed time
@@ -3063,31 +3063,31 @@
  * 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)
     return;
 if (!contType)
     contType = "text/html";
 if (!cookieName)
     cookieName = hUserCookie();
 
 addHttpHeaders();
 cartWriteCookie(cart, cookieName);
-printf("Content-Type: %s\n\n", contType);
+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();
 
@@ -3126,31 +3126,31 @@
     }
 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)
         {
-        puts("Content-Type: text/html\n");
+        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);