c3788110e8d865774fff8197cf5b47350472ba64
chmalee
  Tue Aug 4 11:26:55 2026 -0700
Fix hubSpace hook error handling and nested hub path handling, refs #37964

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git src/hg/hgHubConnect/hooks/pre-create.c src/hg/hgHubConnect/hooks/pre-create.c
index e2dbde8f761..c9d3967fa52 100644
--- src/hg/hgHubConnect/hooks/pre-create.c
+++ src/hg/hgHubConnect/hooks/pre-create.c
@@ -93,33 +93,32 @@
         // our JS code also sets 'fileName' - try all to handle resumed uploads with old metadata)
         char *reqFileName = jsonQueryString(req, "", "Event.Upload.MetaData.fileName", NULL);
         if (!reqFileName)
             reqFileName = jsonQueryString(req, "", "Event.Upload.MetaData.filename", NULL);
         if (!reqFileName)
             reqFileName = jsonQueryString(req, "", "Event.Upload.MetaData.name", NULL);
         if (!reqFileName)
             {
             errAbort("No filename found in upload metadata (checked fileName, filename, and name)");
             }
         char *reqParentDir = jsonQueryString(req, "", "Event.Upload.MetaData.parentDir", NULL);
         // Trim first, a hub name with a trailing space is impossible to spot in an error
         // message, then check what is left. The browser does this too, but hubtools and
         // any other tus client post here directly
         reqParentDir = normalizeParentDir(reqParentDir);
-        if (reqParentDir && isEmpty(reqParentDir))
-            errAbort("Hub name for file '%s' is only whitespace, please give the hub a name",
-                    reqFileName);
+        if (isEmpty(reqParentDir))
+            errAbort("No hub name for file '%s', please give the hub a name", reqFileName);
         if (!isValidParentDir(reqParentDir))
             errAbort("Hub name '%s' for file '%s' can only contain letters, numbers, periods "
                     "and underscores, in '/' separated components. Please rename the hub.",
                     reqParentDir, reqFileName);
         boolean isHubToolsUpload = FALSE;
         char *hubtoolsStr = jsonQueryString(req, "", "Event.Upload.MetaData.hubtools", NULL);
         if (hubtoolsStr)
             isHubToolsUpload = sameString(hubtoolsStr, "TRUE") || sameString(hubtoolsStr, "true");
         // Check for allowOverwrite metadata from JavaScript (for hub.txt overwrites)
         char *allowOverwriteStr = jsonQueryString(req, "", "Event.Upload.MetaData.allowOverwrite", NULL);
         boolean allowOverwrite = (allowOverwriteStr && (sameString(allowOverwriteStr, "TRUE") || sameString(allowOverwriteStr, "true")));
         boolean forceOverwrite = isHubToolsUpload || allowOverwrite;
         long currQuota = checkUserQuota(userName);
         long newQuota = currQuota + reqFileSize;
         long maxQuota = getMaxUserQuota(userName);
@@ -130,31 +129,33 @@
         char *reqFileType = jsonQueryString(req, "", "Event.Upload.MetaData.fileType", NULL);
         if (!isFileTypeRecognized(reqFileType))
             {
             errAbort("File type '%s' for file '%s' is not accepted at this time", reqFileType, reqFileName);
             }
         char *reqGenome = jsonQueryString(req, "", "Event.Upload.MetaData.genome", NULL);
         if (!reqGenome)
             {
             errAbort("Genome selection is NULL for file '%s' is invalid. Please choose the correct genome", reqFileName);
             }
 
         // Block 2bit uploads whose genome name collides with a UCSC native database or GenArk hub.
         if (sameOk(reqFileType, "2bit") && reqGenome[0] &&
             (hDbExists(reqGenome) || isGenArk(reqGenome)))
             {
-            char *hubName = reqParentDir ? hubNameFromPath(reqParentDir) : "";
+            // existingHubTypeForDir looks up the hub's row at the top level, so give it
+            // the hub component of parentDir rather than a nested subdirectory
+            char *hubName = hubRootFromParentDir(reqParentDir);
             char *existingHubType = existingHubTypeForDir(userName, hubName);
             if (!sameOk(existingHubType, "assemblyHub"))
                 errAbort(HUB_GENOME_COLLISION_ERR_FMT, reqGenome, reqGenome);
             }
 
         // we've passed all the checks so we can return that we are good to upload the file
         if (exitStatus == 0)
             {
             // set the location of the upload to the location it will ultimately live
             char *location = setUploadPath(userName, reqFileName, reqParentDir, forceOverwrite);
             if (!location)
                 {
                 errAbort("Error setting upload path in pre-create for file '%s'. This is an"
                         " issue with our server, please email genome-www@soe.ucsc.edu with your"
                         " userName so we can investigate.", reqFileName);
@@ -168,31 +169,31 @@
             jsonObjectAdd(changeObj, "Storage", pathObj);
             jsonObjectAdd(changeObj, "ID", newJsonString(makeRandomKey(128)));
             jsonObjectAdd(response, "ChangeFileInfo", changeObj);
             fillOutHttpResponseSuccess(response);
             }
         }
     // pop the handlers before handling the error, so an errAbort in the error
     // path cannot longjmp back into this same block
     errCatchEnd(errCatch);
     if (errCatch->gotError)
         {
         // App-level reject: tusd treats exit 0 + RejectUpload=true as a clean
         // rejection and forwards our HTTPResponse body verbatim. Non-zero
         // would be wrapped in "ERR_INTERNAL_SERVER_ERROR ... from hook
         // endpoint: ..." which buries the real message.
-        rejectUpload(response, errCatch->message->string);
+        rejectUpload(response, "%s", errCatch->message->string);
         exitStatus = 0;
         }
     }
 // always print a response no matter what
 jsonPrintToFile(response, NULL, stdout, 0);
 return 0;
 }
 
 int main(int argc, char *argv[])
 /* Process command line. */
 {
 optionInit(&argc, argv, options);
 if (argc != 1)
     usage();
 return preCreate();