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.