fix(idaa): add Novi fetch timeout and auto-retry on network errors

- 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 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-19 15:29:14 -04:00
parent 2855e091f7
commit e921ca973f

View File

@@ -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<void>((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() {
</p>
{:else}
<p class="text-sm">
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.
</p>
{/if}
<p class="text-xs italic text-gray-500">
If this keeps happening, try "Clear Cache &amp; Reload" or contact technical support.
Try "Try Again" first. If the problem persists, use "Clear Cache &amp; Reload".
If nothing works, close this page and reopen IDAA from the menu on idaa.org.
</p>
<div class="flex flex-row flex-wrap items-center justify-center gap-2">
<!-- Try Again: unlatches verify_failed_for_uuid and re-runs Effect 2 — no reload needed -->
@@ -591,7 +627,8 @@ function handle_verify_retry() {
{#if $ae_loc.iframe}
<p class="text-xs italic text-gray-500">
If you just opened this page, try reloading. If the problem persists, try "Clear Cache &amp; Reload".
If you just opened this page, try reloading. If the problem persists, try
"Clear Cache &amp; Reload", or close this page and reopen IDAA from the menu on idaa.org.
</p>
<div class="flex flex-row flex-wrap items-center justify-center gap-2">
<!-- WHY: In iframe mode the Novi UUID is passed via URL param on first load.