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(""); printf(""); puts(""); @@ -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("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."); 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);