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);