From e921ca973f7bd8d478ddec0f9f49bf0d5afb47ca Mon Sep 17 00:00:00 2001
From: Scott Idem
Date: Tue, 19 May 2026 15:29:14 -0400
Subject: [PATCH] fix(idaa): add Novi fetch timeout and auto-retry on network
errors
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Wrap Novi API fetch in AbortController with 12s hard timeout — prevents
verify_in_flight from getting stuck if Novi's server hangs with no response
- Auto-retry once (after 3s) on network errors (TypeError: Failed to fetch)
and timeouts (AbortError) — these are almost always transient cellular/WiFi
blips and previously hard-failed with no second chance
- Rate-limit retries (429) already had a 10s wait; network retry is separate
- Update status message to "Connection issue — retrying..." during network retry
- Update error panel hint to suggest closing/reopening the Novi page as last resort
- Update Access Denied hint with same guidance
Co-Authored-By: Claude Sonnet 4.6
---
src/routes/idaa/(idaa)/+layout.svelte | 53 +++++++++++++++++++++++----
1 file changed, 45 insertions(+), 8 deletions(-)
diff --git a/src/routes/idaa/(idaa)/+layout.svelte b/src/routes/idaa/(idaa)/+layout.svelte
index ea6d374c..9a2d8485 100644
--- a/src/routes/idaa/(idaa)/+layout.svelte
+++ b/src/routes/idaa/(idaa)/+layout.svelte
@@ -271,7 +271,14 @@ $effect(() => {
* Verifies a Novi UUID against the Novi API and sets permissions accordingly.
* "All or nothing" — if no API key is configured or the call fails, access is denied.
* Called from within untrack(), so store writes here will not trigger reactive loops.
- * On a 429 rate-limit response, waits 10 seconds and retries once before failing.
+ *
+ * Retry policy:
+ * 429 (rate limited) → wait 10s, retry once.
+ * Network error / timeout → wait 3s, retry once.
+ * Anything else (4xx, 5xx, empty 200) → fail immediately.
+ *
+ * The Novi fetch is wrapped in an AbortController with a 12s hard timeout.
+ * Without it, a hung Novi server leaves verify_in_flight=true indefinitely.
*/
async function verify_novi_uuid(
uuid: string,
@@ -302,10 +309,21 @@ async function verify_novi_uuid(
try {
const headers = new Headers();
headers.append('Authorization', `Basic ${api_key}`);
- const response = await fetch(`${api_root_url}/customers/${uuid}`, {
- method: 'GET',
- headers
- });
+
+ // Hard timeout: abort if Novi doesn't respond within 12 seconds.
+ // Prevents verify_in_flight from getting stuck on a hung server.
+ const abort_ctrl = new AbortController();
+ const abort_timeout_id = setTimeout(() => abort_ctrl.abort(), 12_000);
+ let response: Response;
+ try {
+ response = await fetch(`${api_root_url}/customers/${uuid}`, {
+ method: 'GET',
+ headers,
+ signal: abort_ctrl.signal
+ });
+ } finally {
+ clearTimeout(abort_timeout_id);
+ }
if (response.status === 429) {
if (is_retry) {
@@ -393,6 +411,23 @@ async function verify_novi_uuid(
$idaa_loc.bb.qry__hidden = 'not_hidden';
$idaa_loc.bb.qry__enabled = 'enabled';
} catch (error) {
+ // Network errors (TypeError: Failed to fetch) and AbortError (our 12s timeout) get one
+ // automatic retry after a short wait — they are almost always transient (cellular drop,
+ // hotel WiFi hiccup, momentary Novi server lag).
+ const is_network_or_timeout =
+ error instanceof TypeError ||
+ (error instanceof Error && error.name === 'AbortError');
+ if (is_network_or_timeout && !is_retry) {
+ const reason = error instanceof Error && error.name === 'AbortError'
+ ? 'timed out (no response in 12s)'
+ : 'network error';
+ console.warn(`IDAA Layout: Novi verification ${reason} for ${uuid}. Retrying in 3s...`);
+ verifying_status_msg = 'Connection issue — retrying...';
+ await new Promise((resolve) => setTimeout(resolve, 3_000));
+ await verify_novi_uuid(uuid, api_key, api_root_url, true);
+ return;
+ }
+
// Verification failed — all-or-nothing means deny access.
console.error(
`IDAA Layout: Novi UUID verification failed for ${uuid}:`,
@@ -488,11 +523,12 @@ function handle_verify_retry() {
{:else}
- We were unable to connect to the membership directory. This is likely a temporary issue — it may not be a problem with your membership or access.
+ We were unable to connect to the membership directory. This is usually a temporary network issue — it is not a problem with your membership or access.
{/if}
- If this keeps happening, try "Clear Cache & Reload" or contact technical support.
+ Try "Try Again" first. If the problem persists, use "Clear Cache & Reload".
+ If nothing works, close this page and reopen IDAA from the menu on idaa.org.
@@ -591,7 +627,8 @@ function handle_verify_retry() {
{#if $ae_loc.iframe}
- If you just opened this page, try reloading. If the problem persists, try "Clear Cache & Reload".
+ If you just opened this page, try reloading. If the problem persists, try
+ "Clear Cache & Reload", or close this page and reopen IDAA from the menu on idaa.org.