081dc1a2de216d74a28252747d593c2b1288f251
max
  Tue May 26 02:06:08 2026 -0700
hubApi/botDelay: return JSON 500 when bottleneck server is unreachable

abortAndExplainConnectFail() previously called exit(0) directly after
writing raw HTML, which bypassed every error handler and produced a
broken response for JSON API clients.  Change it to errAbort() so it
can be caught by errCatch.

Wrap both hgBotDelayTimeFrac() call sites in hubApi (the global delay
in main() and the BLAT-specific extra delay in apiBlat()) in errCatch
blocks that call apiErrAbort(500) on failure, giving callers a proper
JSON error rather than an HTML fragment or a silent exit.

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 63b2c391eb5..907623b2446 100644
--- src/hg/hubApi/blat.c
+++ src/hg/hubApi/blat.c
@@ -297,31 +297,39 @@
  * For everyone else, an apiKey must be present; validity was already
  * checked in main(). */
 char *apiKey = cgiOptionalString(argApiKey);
 if (isEmpty(apiKey) && !botException() && !botExceptionUserAgent())
     apiErrAbort(err403, err403Msg,
         "/blat requires an '%s' URL parameter. "
         "Generate one under My Data > My Track Hubs > Hub Development: API Key, "
         "then add it to this API call as apiKey=xxxxx. "
         "Contact us if you need assistance.", argApiKey);
 
 /* Apply a per-BLAT bottleneck penalty on top of the global hubApi delay
  * already paid in main().  Bottleneck key is the apiKey (see botDelay.c),
  * so heavy users throttle themselves instead of starving everyone. */
 char *blatDelayStr = cfgOptionDefault("hubApi.blatDelayFraction", NULL);
 double blatDelayFraction = blatDelayStr ? atof(blatDelayStr) : blatDelayFractionDefault;
-int extraDelay = hgBotDelayTimeFrac(blatDelayFraction);
+int extraDelay = 0;
+struct errCatch *bnErrCatch = errCatchNew();
+if (errCatchStart(bnErrCatch))
+    extraDelay = hgBotDelayTimeFrac(blatDelayFraction);
+errCatchEnd(bnErrCatch);
+if (bnErrCatch->gotError)
+    apiErrAbort(err500, err500Msg, "bottleneck server unavailable: %s",
+        bnErrCatch->message->string);
+errCatchFree(&bnErrCatch);
 if (extraDelay > 0)
     sleep1000(extraDelay);
 
 /* Query-type subcommand comes from the URL path, like /getData/track.
  * Apache rewrites /blat/<type>?... onto PATH_INFO=/blat/<type>, so
  * words[1] holds the user-supplied subcommand. */
 char *type = words[1];
 
 char *genome = cgiOptionalString(argGenome);
 char *userSeq = cgiOptionalString(argUserSeq);
 char *format = cgiOptionalString(argFormat);
 char *hubUrl = cgiOptionalString(argHubUrl);
 
 if (isEmpty(genome))
     apiErrAbort(err400, err400Msg,