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/lib/errAbort.c src/lib/errAbort.c
index 19e64660806..d63b2944691 100644
--- src/lib/errAbort.c
+++ src/lib/errAbort.c
@@ -15,30 +15,31 @@
  * This file is copyright 2002 Jim Kent, but license is hereby
  * granted for all use - public, private or commercial. */
 
 // developer: this include is for an occasionally useful means of getting stack info without
 // crashing
 // however, it is not supported on cygwin.  Conditionally compile this in when desired.
 //#define BACKTRACE_EXISTS
 #ifdef BACKTRACE_EXISTS
 #include <execinfo.h>
 #endif///def BACKTRACE_EXISTS
 #include <pthread.h>
 #include "common.h"
 #include "hash.h"
 #include "dystring.h"
 #include "errAbort.h"
+#include "cheapcgi.h"
 
 // errAbort can optionally print a Content-type line and copy errors to stdout, so 
 // error messages don't lead to a 500 error but are shown in the web browser
 // directly. 
 static boolean doContentType = FALSE;
 
 #define maxWarnHandlers 20
 #define maxAbortHandlers 12
 struct perThreadAbortVars
 /* per thread variables for abort and warn */
     {
     boolean debugPushPopErr;        // generate stack dump on push/pop error
     boolean errAbortInProgress;     /* Flag to indicate that an error abort is in progress.
                                       * Needed so that a warn handler can tell if it's really
                                       * being called because of a warning or an error. */
@@ -46,31 +47,31 @@
     int warnIx;
     AbortHandler abortArray[maxAbortHandlers];
     int abortIx;
     };
 
 static struct perThreadAbortVars *getThreadVars();  // forward declaration
 
 static void defaultVaWarn(char *format, va_list args)
 /* Default error message handler. */
 {
 if (format == NULL)
     return;
         
 if (doContentType)
     {
-    puts("Content-type: text/html\n");
+    cgiPrintContentType("text/html");
     puts("Error: ");
         
     // Need to destroy < and > in format AND args, to make XSS impossible.
     va_list args_copy;
     
     // first output message to stderr, as before
     va_copy(args_copy, args); // vfprintf() & co cannot be called twice in a row without a va_copy
     vfprintf(stderr, format, args);
     va_end(args_copy);
 
     va_copy(args_copy, args);
     int needed = vsnprintf(NULL, 0, format, args_copy); // get size of buffer
     va_end(args_copy);
     if (needed < 0)
         {