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:
@@ -271,7 +271,14 @@ $effect(() => {
|
|||||||
* Verifies a Novi UUID against the Novi API and sets permissions accordingly.
|
* 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.
|
* "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.
|
* 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(
|
async function verify_novi_uuid(
|
||||||
uuid: string,
|
uuid: string,
|
||||||
@@ -302,10 +309,21 @@ async function verify_novi_uuid(
|
|||||||
try {
|
try {
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append('Authorization', `Basic ${api_key}`);
|
headers.append('Authorization', `Basic ${api_key}`);
|
||||||
const response = await fetch(`${api_root_url}/customers/${uuid}`, {
|
|
||||||
method: 'GET',
|
// Hard timeout: abort if Novi doesn't respond within 12 seconds.
|
||||||
headers
|
// 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 (response.status === 429) {
|
||||||
if (is_retry) {
|
if (is_retry) {
|
||||||
@@ -393,6 +411,23 @@ async function verify_novi_uuid(
|
|||||||
$idaa_loc.bb.qry__hidden = 'not_hidden';
|
$idaa_loc.bb.qry__hidden = 'not_hidden';
|
||||||
$idaa_loc.bb.qry__enabled = 'enabled';
|
$idaa_loc.bb.qry__enabled = 'enabled';
|
||||||
} catch (error) {
|
} 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.
|
// Verification failed — all-or-nothing means deny access.
|
||||||
console.error(
|
console.error(
|
||||||
`IDAA Layout: Novi UUID verification failed for ${uuid}:`,
|
`IDAA Layout: Novi UUID verification failed for ${uuid}:`,
|
||||||
@@ -488,11 +523,12 @@ function handle_verify_retry() {
|
|||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-sm">
|
<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>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="text-xs italic text-gray-500">
|
<p class="text-xs italic text-gray-500">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-row flex-wrap items-center justify-center gap-2">
|
<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 -->
|
<!-- 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}
|
{#if $ae_loc.iframe}
|
||||||
<p class="text-xs italic text-gray-500">
|
<p class="text-xs italic text-gray-500">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-row flex-wrap items-center justify-center gap-2">
|
<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.
|
<!-- WHY: In iframe mode the Novi UUID is passed via URL param on first load.
|
||||||
|
|||||||
Reference in New Issue
Block a user