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/hgSession/hgSession.c src/hg/hgSession/hgSession.c index 1ac413e611e..8c062c68dc5 100644 --- src/hg/hgSession/hgSession.c +++ src/hg/hgSession/hgSession.c @@ -746,31 +746,31 @@ "TARGET=_BLANK>Session Gallery.\n", dyUrl->string); printf("\n"); dyStringFree(&dyUrl); } void doMainPage(char *userName, char *message) /* Login status/links and session controls. */ { if (sessionNewPageActive()) { doMainPageNew(userName, message); return; } cspWriteResponseHeader(); -puts("Content-Type:text/html\n"); +cgiPrintContentType("text/html"); if (loginSystemEnabled() || wikiLinkEnabled()) { if (userName) welcomeUser(userName); else offerLogin(); if (isNotEmpty(message)) { if (cartVarExists(cart, hgsDoSessionDetail)) webNewSection("Session Details"); else webNewSection("Updated Session"); puts(message); } showSessionControls(userName, TRUE, TRUE); @@ -1039,57 +1039,57 @@ cartCheckForCustomTracks(cart, dyMessage); } else dyStringPrintf(dyMessage, "Sorry, required table %s does not exist yet in the central " "database (%s). Please ask a developer to create it using " "kent/src/hg/lib/namedSessionDb.sql .", namedSessionTable, sqlGetDatabase(conn)); hDisconnectCentral(&conn); return dyStringCannibalize(&dyMessage); } static void saveSessionJsonError(struct sqlConnection *conn, char *message) /* Emit a JSON error response for the "Share a link" AJAX endpoints and disconnect. */ { -puts("Content-Type:application/json\n"); +cgiPrintContentType("application/json"); printf("{\"error\": \"%s\"}\n", jsonStringEscape(message)); hDisconnectCentral(&conn); } static boolean namedSessionExists(struct sqlConnection *conn, char *encUserName, char *encSessionName) /* Is there already a session by this name for this user? Both names must be encoded the way they * are stored, i.e. through cgiEncodeFull(). */ { char query[1024]; sqlSafef(query, sizeof query, "select count(*) from %s where userName = '%s' and sessionName = '%s'", namedSessionTable, encUserName, encSessionName); return sqlQuickNum(conn, query) > 0; } static void saveSessionJsonResult(struct sqlConnection *conn, char *encUserName, char *encSessionName, char *sessionName, char *warning) /* Emit {"name": ..., "url": ...} for the "Share a link" AJAX endpoints and disconnect. * sessionName is the human-readable (decoded) name; the client uses it as the rename "old name". * warning (may be NULL) is added as "warning" for something that went wrong alongside a save that * did succeed, such as a thumbnail the server could not build. */ { struct dyString *dyUrl = dyStringNew(0); addSessionLink(dyUrl, encUserName, encSessionName, FALSE, TRUE); -puts("Content-Type:application/json\n"); +cgiPrintContentType("application/json"); printf("{\"name\": \"%s\", \"url\": \"%s\"", jsonStringEscape(sessionName), jsonStringEscape(dyUrl->string)); if (isNotEmpty(warning)) printf(", \"warning\": \"%s\"", jsonStringEscape(warning)); puts("}"); dyStringFree(&dyUrl); hDisconnectCentral(&conn); } static int sessionSharedLevel(struct sqlConnection *conn, char *encUserName, char *encSessionName) /* Return the sharing level of this user's session: 0 private, 1 shared by link, 2 in the public * listing. Returns -1 when the user has no session by that name, which the shared column cannot * express (it is NOT NULL), so the AJAX endpoints can say so instead of reporting a no-op as a * success. */ { @@ -1113,31 +1113,31 @@ void doAnonNameJson() /* AJAX endpoint that reserves a fresh, guaranteed-unique anonymous snapshot name and returns it as * JSON {"name": ...} WITHOUT saving anything. The top-right "Share a link" dialog calls this on open * so it can show the exact link as a preview before the user commits, while keeping name generation * server-side (unique, crypto-strong) for every anonymous link. */ { struct sqlConnection *conn = hConnectCentral(); cartRemove(cart, hgsDoAnonName); if (!sqlTableExists(conn, namedSessionTable)) { saveSessionJsonError(conn, "Required session table does not exist in the central database."); return; } char *name = snapshotNewName(conn, "l"); -puts("Content-Type:application/json\n"); +cgiPrintContentType("application/json"); printf("{\"name\": \"%s\"}\n", jsonStringEscape(name)); hDisconnectCentral(&conn); } void doSaveSessionJson(char *userName) /* AJAX endpoint behind the "Share a link" menu button. Save the current cart as a named session * and print JSON {"name": , "url": }. When the user is not logged * in (or hgsShareAnon is set), save under the reserved anonymous user "l" with a random token * name. When logged in, the caller supplies the name (a typed name, or a random internal * "_XXXXXXXX" name generated client-side). Saved shared by link so the link works for anyone. * Reuses saveCartAsSession() and addSessionLink(). */ { struct sqlConnection *conn = hConnectCentral(); if (!sqlTableExists(conn, namedSessionTable)) { @@ -1237,31 +1237,31 @@ { /* Logged-in callers always supply a name: the caller either typed one or generated a random * internal "_XXXXXXXX" name client-side (sessRandomShareName in hgSession.js, shared by the * top-right "Share a link" menu in topLinks.js), so we no longer auto-name here. */ if (isEmpty(sessionName)) { saveSessionJsonError(conn, "Please provide a name for this session."); return; } encUserName = cgiEncodeFull(userName); encSessionName = cgiEncodeFull(sessionName); /* The Share dialog sets failIfExists when the user typed a custom name, so it can warn before * clobbering an existing session of theirs. Report the clash instead of overwriting. */ if (failIfExists && namedSessionExists(conn, encUserName, encSessionName)) { - puts("Content-Type:application/json\n"); + cgiPrintContentType("application/json"); printf("{\"exists\": true}\n"); hDisconnectCentral(&conn); return; } } saveCartAsSession(conn, encUserName, encSessionName, 1); /* shared by link */ saveSessionJsonResult(conn, encUserName, encSessionName, sessionName, NULL); } void doRenameSessionJson(char *userName) /* AJAX endpoint for the "Specify name" step of the Share dialog: rename an existing session * (hgsOldSessionName -> hgsNewSessionName) under the current user. Logged-in only. Rejects a * name already in use rather than overwriting it. Returns {"name","url"} or {"error"}. */ { @@ -2519,31 +2519,31 @@ hashFree(&dbConnCache); } hDisconnectCentral(&conn); } jsonWriteListEnd(jw); // sessions } void doMainPageNew(char *userName, char *message) /* Render the experimental client-rendered Sessions page: framework header (gold "My Sessions" * band), the experimental banner, an empty #sessionApp container, and the hgSessionData JSON that * hgSession.js reads to build the UI. */ { if (isNotEmpty(cartOptionalString(cart, "measureTiming"))) hgSessionTiming = perfTimerNew(); /* times the page; emitted as hgSessionData.timing */ cspWriteResponseHeader(); -puts("Content-Type:text/html\n"); +cgiPrintContentType("text/html"); cartWebStart(cart, NULL, "My Sessions"); jsInit(); jsIncludeDataTablesLibs(); webIncludeResourceFile("gbModern.css"); webIncludeResourceFile("hgSession.css"); jsIncludeFile("hgSession.js", NULL); printSessionNewPageBanner(TRUE); if (isNotEmpty(message)) printf("
%s
\n", message); printf("
\n"); struct jsonWrite *jw = jsonWriteNew(); jsonWriteObjectStart(jw, NULL); @@ -2553,31 +2553,31 @@ perfTimerJson(hgSessionTiming, jw, "timing"); jsonWriteObjectEnd(jw); jsInlineF("var hgSessionData = %s;\n", jw->dy->string); jsonWriteFree(&jw); perfTimerFree(&hgSessionTiming); cartWebEnd(); } /* ---- JSON action endpoints for the experimental page's inline table actions ---- */ static void saveSessionJsonOk(struct sqlConnection *conn, char *extraFields) /* Emit {"success": true[, ]} and disconnect. extraFields (may be NULL) is inserted * verbatim after "success": true, e.g. ", \"shared\": 2". */ { -puts("Content-Type:application/json\n"); +cgiPrintContentType("application/json"); printf("{\"success\": true%s}\n", extraFields ? extraFields : ""); hDisconnectCentral(&conn); } void doDeleteSessionJson(char *userName) /* AJAX: delete the session named by hgsOldSessionName under the current user. */ { struct sqlConnection *conn = hConnectCentral(); char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsOldSessionName, ""))); if (isEmpty(userName)) { saveSessionJsonError(conn, "Please log in and try again."); return; } if (isEmpty(sessionName)) { saveSessionJsonError(conn, "No session was specified."); return; } char *encUserName = cgiEncodeFull(userName); char *encSessionName = cgiEncodeFull(sessionName);