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
- 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.
- 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.