79e7ed894b39f65202ef820ae81af514f62e43dd
max
  Wed May 20 13:38:35 2026 -0700
hubApi/blat: errCatch gfServer calls, fix comment, maxSeqCount from hg.conf, Content-Type spacing

- Wrap gfServer block (mustOpen→gfDisconnect) in errCatch so a down or
unreachable BLAT server returns a JSON 500 instead of a bare Apache error.
- Fix comment: hgBlat's CGI interface and behavior remain unchanged (it
was refactored internally in the previous commit).
- Replace hardcoded maxSeqCount=25 with cfgOptionDefault("hgBlat.maxSequenceCount")
to share the same hg.conf knob as hgBlat itself.
- Remove spurious space in "Content-Type:text/plain" to match rest of hubApi.

refs #36315

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff --git src/hg/hubApi/blat.c src/hg/hubApi/blat.c
index 81ee7f2b58c..63b2c391eb5 100644
--- src/hg/hubApi/blat.c
+++ src/hg/hubApi/blat.c
@@ -1,19 +1,18 @@
 /* blat - /blat endpoint: run a BLAT against an assembly's gfServer and
  * return PSL hits as JSON (or PSL text).  This is the API-callable twin
- * of hgBlat?output=json; hgBlat itself remains unchanged for backwards
- * compatibility.
+ * of hgBlat?output=json; hgBlat's CGI interface and behavior remain unchanged.
  *
  * NOTE: Much of the alignment logic here (server lookup, sequence filtering,
  * gfAlign* calls, temp-file round-trip) is derived from hgBlat.c.  If you
  * fix a bug or change behaviour there, check whether this file needs the
  * same fix.  See also the reciprocal note in hgBlat.c. */
 
 #include "dataApi.h"
 #include "blatServers.h"
 #include "fa.h"
 #include "dnautil.h"
 #include "dnaseq.h"
 #include "psl.h"
 #include "trashDir.h"
 #include "genoFind.h"
 #include "trackHub.h"
@@ -347,45 +346,54 @@
 
 struct blatType bt;
 parseTypeArg(type, seqList, &bt);
 filterSequences(seqList, &bt);
 
 struct blatServerParams *st = findBlatServer(genome, bt.isTx);
 if (st == NULL)
     apiErrAbort(err400, err400Msg,
         "no %s BLAT server configured for genome='%s'",
         bt.isTx ? "translated" : "DNA", genome);
 
 /* Run alignments into a temp pslx, then read it back to drive output.
  * Mirrors hgBlat's strategy so we benefit from the same gfOutputPsl path. */
 struct tempName pslTn;
 trashDirFile(&pslTn, "apiBlat", "apiBlat", ".pslx");
-FILE *f = mustOpen(pslTn.forCgi, "w");
+
+int maxSeqCount = 25;
+char *optionMaxSeqCount = cfgOptionDefault("hgBlat.maxSequenceCount", NULL);
+if (isNotEmpty(optionMaxSeqCount))
+    maxSeqCount = atoi(optionMaxSeqCount);
+
+FILE *f = NULL;
+struct errCatch *ec = errCatchNew();
+if (errCatchStart(ec))
+    {
+    f = mustOpen(pslTn.forCgi, "w");
     struct gfOutput *gvo = gfOutputPsl(0, bt.qIsProt, FALSE, f, FALSE, TRUE);
     pslxWriteHead(f, bt.qType, bt.tType);
 
     struct gfConnection *conn = gfConnect(st->host, st->port,
         trackHubDatabaseToGenome(st->db), st->genomeDataDir);
     struct hash *tFileCache = gfFileCacheNew();
     int minMatch = 0;  /* let gfServer decide; matches hgBlat allResults path */
 
     struct dnaSeq *seq;
     int singleMax = bt.isTx ? 10000 : 75000;
     int totalMax  = singleMax * 2.5;
     int total = 0;
     int seqCount = 0;
-int maxSeqCount = 25;
     for (seq = seqList; seq != NULL; seq = seq->next)
         {
         if (++seqCount > maxSeqCount)
             break;
         if (seq->size <= 0 || seq->size > singleMax)
             continue;
         total += seq->size;
         if (total > totalMax)
             break;
         if (bt.isTx)
             {
             if (bt.isTxTx)
                 {
                 gfAlignTransTrans(conn, st->nibDir, seq, FALSE, 5, tFileCache, gvo,
                     !bt.txTxBoth);
@@ -396,32 +404,43 @@
                         FALSE);
                     }
                 }
             else
                 gfAlignTrans(conn, st->nibDir, seq, 5, tFileCache, gvo);
             }
         else
             {
             gfAlignStrand(conn, st->nibDir, seq, FALSE, minMatch, tFileCache, gvo);
             reverseComplement(seq->dna, seq->size);
             gfAlignStrand(conn, st->nibDir, seq, TRUE, minMatch, tFileCache, gvo);
             }
         gfOutputQuery(gvo, f);
         }
     carefulClose(&f);
+    f = NULL;
     gfFileCacheFree(&tFileCache);
     gfDisconnect(&conn);
+    }
+errCatchEnd(ec);
+if (ec->gotError)
+    {
+    if (f != NULL)
+        carefulClose(&f);
+    remove(pslTn.forCgi);
+    apiErrAbort(err500, err500Msg, "BLAT server error: %s", ec->message->string);
+    }
+errCatchFree(&ec);
 
 struct lineFile *lf = pslFileOpen(pslTn.forCgi);
 struct psl *pslList = NULL, *psl;
 while ((psl = pslNext(lf)) != NULL)
     slAddHead(&pslList, psl);
 lineFileClose(&lf);
 slReverse(&pslList);
 
 if (sameWordOk(format, "text") || sameWordOk(format, "psl"))
     writePslOutput(pslList, &bt);
 else if (sameWordOk(format, "hgblat"))
     writeLegacyJsonOutput(pslList, st->db);
 else
     writeJsonOutput(pslList, st->db, hubUrl, type, &bt);
 }