ef8d1004e3d984933a79686a7369d74825886c48
max
  Fri Sep 4 11:35:31 2026 -0700
snapshotSession: reject a snapshot missing its required var

A "blat" snapshot is a dead link without blatLastBigBed (the pinned bigPsl).
That variable is set by an async hgc buildBigPsl call, so a share clicked
before the build finished - or after it failed - would have minted a link
that reopens to nothing.  A snapshotType may now name a requiredVar, and
doSaveSessionJson refuses the save (asking the caller to retry) instead of
handing out a broken link.  Found in code review of the BLAT share wiring.

refs #38197

diff --git src/hg/lib/snapshotSession.c src/hg/lib/snapshotSession.c
index 3eabd55d6ab..b85d37d18e8 100644
--- src/hg/lib/snapshotSession.c
+++ src/hg/lib/snapshotSession.c
@@ -17,45 +17,53 @@
 #include "snapshotSession.h"
 
 /* Bits of randomness in a server-generated snapshot token.  128 bits -> ~24 URL-safe chars, so
  * collisions are astronomically unlikely; snapshotNewName() also checks the DB and retries, so the
  * name is guaranteed unique regardless. */
 #define snapshotTokenBits 128
 
 /* ---- Registry of snapshot types ------------------------------------------------------------- */
 
 /* BLAT: a single alignment (or the hgBlat results table) rebuilds from just the pinned bigPsl file;
  * the query sequence lives inside the bigPsl, so no .fa/.pslx is needed.  "db" is added implicitly. */
 static char *blatVars[] = { "blatLastBigBed", NULL };
 
 static struct snapshotType snapshotTypes[] =
     {
-    { "blat", blatVars },
+    { "blat", blatVars, "blatLastBigBed" },
     };
 
 struct snapshotType *snapshotTypeFind(char *name)
 /* Return the registered snapshot type, or NULL if name is not a known type. */
 {
 if (isEmpty(name))
     return NULL;
 int i;
 for (i = 0;  i < ArraySize(snapshotTypes);  i++)
     if (sameString(name, snapshotTypes[i].name))
         return &snapshotTypes[i];
 return NULL;
 }
 
+boolean snapshotHasRequired(struct snapshotType *type, struct cart *cart)
+/* Return FALSE when type declares a requiredVar that is missing/empty in cart. */
+{
+if (type == NULL || isEmpty(type->requiredVar))
+    return TRUE;
+return isNotEmpty(cartOptionalString(cart, type->requiredVar));
+}
+
 boolean snapshotIsSnapshotName(char *sessionName)
 /* Return TRUE if sessionName is a snapshot name (starts with the "__" prefix). */
 {
 return sessionName != NULL && startsWith(snapshotNamePrefix, sessionName);
 }
 
 static char *snapshotSessionDir(char *sessionDataDir, char *encUserName, char *encSessionName)
 /* Alloc and return the durable data directory for one snapshot, or NULL if sessionDataDir is empty.
  * Like sessionData's sessionDirFromNames but with two extra hash levels drawn from the session name,
  * so a single high-volume user - the anonymous "l", which owns most snapshots - never accumulates
  * millions of entries in one directory.  Layout:
  *   sessionDataDir / <2hex md5(user)> / <encUser> / <2hex sess> / <2hex sess> / <8hex md5(session)>
  * Snapshots use their own layout (not sessionDirFromNames), so this never affects normal sessions. */
 {
 if (isEmpty(sessionDataDir))