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</A>.</LI>\n",
dyUrl->string);
printf("</UL>\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": <session name>, "url": <shareable link>}. 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("<div class='gbBanner'>%s</div>\n", message);
printf("<div id='sessionApp' class='gbApp'></div>\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[, <extraFields>]} 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);