From 4a873e8262ea4f9e54816d7e24fbe5c9bb54b051 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Wed, 29 Jul 2026 15:17:04 +0200 Subject: [PATCH 1/3] Don't retry W-ASAP validation errors in React Query Introduce WasapValidationError for config/state validation failures (mode not enabled, no collection selected, etc.) and tell React Query not to retry them, so the UI doesn't spin for ~7s on states that are immediately known to be invalid. Co-Authored-By: Claude Sonnet 4.6 --- .../views/wasap/useWasapPageData.spec.ts | 18 +++++----- .../views/wasap/useWasapPageData.ts | 34 ++++++++++++------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/website/src/components/views/wasap/useWasapPageData.spec.ts b/website/src/components/views/wasap/useWasapPageData.spec.ts index f0ea12411..29ceaeb26 100644 --- a/website/src/components/views/wasap/useWasapPageData.spec.ts +++ b/website/src/components/views/wasap/useWasapPageData.spec.ts @@ -2,7 +2,7 @@ import dayjs from 'dayjs'; import { http } from 'msw'; import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { fetchWasapPageData, getLapisFilterForTimeFrame } from './useWasapPageData.ts'; +import { fetchWasapPageData, getLapisFilterForTimeFrame, WasapValidationError } from './useWasapPageData.ts'; import { EXCLUDE_SET_NAME, SEQUENCE_TYPE, @@ -79,7 +79,7 @@ describe('fetchWasapPageData', () => { {}, { mode: WASAP_ANALYSIS_MODE.manual, sequenceType: SEQUENCE_TYPE.nucleotide, mutations: [] }, ), - ).rejects.toThrow("Cannot fetch data, 'manual' mode is not enabled."); + ).rejects.toThrow(new WasapValidationError("Cannot fetch data, 'manual' mode is not enabled.")); }); }); @@ -310,7 +310,7 @@ describe('fetchWasapPageData', () => { timeFrame: VARIANT_TIME_FRAME.all, }, ), - ).rejects.toThrow("Cannot fetch data, 'variant' mode is not enabled."); + ).rejects.toThrow(new WasapValidationError("Cannot fetch data, 'variant' mode is not enabled.")); }); }); @@ -411,7 +411,7 @@ describe('fetchWasapPageData', () => { {}, { mode: WASAP_ANALYSIS_MODE.untracked, sequenceType: SEQUENCE_TYPE.nucleotide }, ), - ).rejects.toThrow("Cannot fetch data, 'untracked' mode is not enabled."); + ).rejects.toThrow(new WasapValidationError("Cannot fetch data, 'untracked' mode is not enabled.")); }); }); @@ -558,7 +558,9 @@ describe('fetchWasapPageData', () => { {}, { mode: WASAP_ANALYSIS_MODE.covSpectrumCollection, collectionId: 42 }, ), - ).rejects.toThrow("Cannot fetch data, 'covSpectrumCollection' mode is not enabled."); + ).rejects.toThrow( + new WasapValidationError("Cannot fetch data, 'covSpectrumCollection' mode is not enabled."), + ); }); test('throws when no collection is selected', async () => { @@ -568,7 +570,7 @@ describe('fetchWasapPageData', () => { {}, { mode: WASAP_ANALYSIS_MODE.covSpectrumCollection, collectionId: undefined }, ), - ).rejects.toThrow('No collection selected'); + ).rejects.toThrow(new WasapValidationError('No collection selected')); }); }); @@ -938,13 +940,13 @@ describe('fetchWasapPageData', () => { await expect( fetchWasapPageData(disabledConfig, {}, { mode: WASAP_ANALYSIS_MODE.collection, collectionId: 1 }), - ).rejects.toThrow("Cannot fetch data, 'collection' mode is not enabled."); + ).rejects.toThrow(new WasapValidationError("Cannot fetch data, 'collection' mode is not enabled.")); }); test('throws when no collection is selected', async () => { await expect( fetchWasapPageData(config, {}, { mode: WASAP_ANALYSIS_MODE.collection, collectionId: undefined }), - ).rejects.toThrow('No collection selected'); + ).rejects.toThrow(new WasapValidationError('No collection selected')); }); }); }); diff --git a/website/src/components/views/wasap/useWasapPageData.ts b/website/src/components/views/wasap/useWasapPageData.ts index 62c7bfb6d..20303e434 100644 --- a/website/src/components/views/wasap/useWasapPageData.ts +++ b/website/src/components/views/wasap/useWasapPageData.ts @@ -24,6 +24,13 @@ import { getLineageFields } from '../../../types/Collection'; import type { FilterObject, Variant } from '../../../types/Collection'; import { validateGenomeOnly } from '../../../util/siloExpressionUtils'; +export class WasapValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'WasapValidationError'; + } +} + /** * Hook that fetches and returns `WasapPageData` for the W-ASAP page, * depending on the analysis mode and analysis mode settings. @@ -38,6 +45,7 @@ export function useWasapPageData( return useQuery({ queryKey: ['wasap', analysis, resistanceMutationsBySet], queryFn: () => fetchWasapPageData(config, resistanceMutationsBySet, analysis), + retry: (_, error) => !(error instanceof WasapValidationError), }); } @@ -64,7 +72,7 @@ export async function fetchWasapPageData( function fetchManualModeData(config: WasapPageConfig, analysis: WasapManualFilter): WasapMutationsData { if (!config.manualAnalysisModeEnabled) { - throw Error("Cannot fetch data, 'manual' mode is not enabled."); + throw new WasapValidationError("Cannot fetch data, 'manual' mode is not enabled."); } return { type: 'mutations', @@ -77,7 +85,7 @@ async function fetchVariantModeData( analysis: WasapVariantFilter, ): Promise { if (!config.variantAnalysisModeEnabled) { - throw Error("Cannot fetch data, 'variant' mode is not enabled."); + throw new WasapValidationError("Cannot fetch data, 'variant' mode is not enabled."); } switch (analysis.signatureType) { case 'computed': @@ -92,7 +100,7 @@ async function fetchVariantComputedModeData( analysis: WasapVariantFilter, ): Promise { if (!config.variantAnalysisModeEnabled) { - throw Error("Cannot fetch data, 'variant' mode is not enabled."); + throw new WasapValidationError("Cannot fetch data, 'variant' mode is not enabled."); } const mutationsWithScore = await getMutationsForVariant( config.clinicalLapis.lapisBaseUrl, @@ -124,10 +132,10 @@ async function fetchVariantPredefinedModeData( analysis: WasapVariantFilter, ): Promise { if (!config.variantAnalysisModeEnabled) { - throw Error("Cannot fetch data, 'variant' mode is not enabled."); + throw new WasapValidationError("Cannot fetch data, 'variant' mode is not enabled."); } if (analysis.collectionId === undefined) { - throw new Error('No collection selected for predefined variant mode.'); + throw new WasapValidationError('No collection selected for predefined variant mode.'); } const collection = await getBackendServiceForClientside().getCollection({ id: String(analysis.collectionId) }); @@ -141,10 +149,12 @@ async function fetchVariantPredefinedModeData( const variant = collection.variants.find((v) => v.name === variantName); if (!variant) { - throw new Error(`Variant "${variantName}" not found in collection ${collection.id}.`); + throw new WasapValidationError(`Variant "${variantName}" not found in collection ${collection.id}.`); } if (variant.type !== 'filterObject') { - throw new Error(`Variant "${variantName}" in collection ${collection.id} is not a filterObject variant.`); + throw new WasapValidationError( + `Variant "${variantName}" in collection ${collection.id} is not a filterObject variant.`, + ); } const mutations = @@ -199,7 +209,7 @@ async function fetchUntrackedModeData( analysis: WasapUntrackedFilter, ): Promise { if (!config.untrackedAnalysisModeEnabled) { - throw Error("Cannot fetch data, 'untracked' mode is not enabled."); + throw new WasapValidationError("Cannot fetch data, 'untracked' mode is not enabled."); } const variantsToExclude = analysis.excludeSet === 'custom' @@ -240,10 +250,10 @@ async function fetchCovSpectrumCollectionModeData( analysis: WasapCovSpectrumCollectionFilter, ): Promise { if (!config.covSpectrumCollectionAnalysisModeEnabled) { - throw Error("Cannot fetch data, 'covSpectrumCollection' mode is not enabled."); + throw new WasapValidationError("Cannot fetch data, 'covSpectrumCollection' mode is not enabled."); } if (analysis.collectionId === undefined) { - throw Error('No collection selected'); + throw new WasapValidationError('No collection selected'); } const collection = await getCollection(config.collectionsApiBaseUrl, analysis.collectionId); @@ -270,10 +280,10 @@ async function fetchCollectionModeData( analysis: WasapCollectionFilter, ): Promise { if (!config.collectionAnalysisModeEnabled) { - throw Error("Cannot fetch data, 'collection' mode is not enabled."); + throw new WasapValidationError("Cannot fetch data, 'collection' mode is not enabled."); } if (analysis.collectionId === undefined) { - throw Error('No collection selected'); + throw new WasapValidationError('No collection selected'); } const collection = await getBackendServiceForClientside().getCollection({ id: String(analysis.collectionId) }); From b20e84845baa8f40bd12e8e2926773f6fe17a799 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Thu, 30 Jul 2026 10:41:03 +0200 Subject: [PATCH 2/3] Fix test assertions to check WasapValidationError type, not just message Co-Authored-By: Claude Sonnet 4.6 --- .../views/wasap/useWasapPageData.spec.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/website/src/components/views/wasap/useWasapPageData.spec.ts b/website/src/components/views/wasap/useWasapPageData.spec.ts index 29ceaeb26..214e2e108 100644 --- a/website/src/components/views/wasap/useWasapPageData.spec.ts +++ b/website/src/components/views/wasap/useWasapPageData.spec.ts @@ -79,7 +79,7 @@ describe('fetchWasapPageData', () => { {}, { mode: WASAP_ANALYSIS_MODE.manual, sequenceType: SEQUENCE_TYPE.nucleotide, mutations: [] }, ), - ).rejects.toThrow(new WasapValidationError("Cannot fetch data, 'manual' mode is not enabled.")); + ).rejects.toThrow(WasapValidationError); }); }); @@ -310,7 +310,7 @@ describe('fetchWasapPageData', () => { timeFrame: VARIANT_TIME_FRAME.all, }, ), - ).rejects.toThrow(new WasapValidationError("Cannot fetch data, 'variant' mode is not enabled.")); + ).rejects.toThrow(WasapValidationError); }); }); @@ -411,7 +411,7 @@ describe('fetchWasapPageData', () => { {}, { mode: WASAP_ANALYSIS_MODE.untracked, sequenceType: SEQUENCE_TYPE.nucleotide }, ), - ).rejects.toThrow(new WasapValidationError("Cannot fetch data, 'untracked' mode is not enabled.")); + ).rejects.toThrow(WasapValidationError); }); }); @@ -558,9 +558,7 @@ describe('fetchWasapPageData', () => { {}, { mode: WASAP_ANALYSIS_MODE.covSpectrumCollection, collectionId: 42 }, ), - ).rejects.toThrow( - new WasapValidationError("Cannot fetch data, 'covSpectrumCollection' mode is not enabled."), - ); + ).rejects.toThrow(WasapValidationError); }); test('throws when no collection is selected', async () => { @@ -570,7 +568,7 @@ describe('fetchWasapPageData', () => { {}, { mode: WASAP_ANALYSIS_MODE.covSpectrumCollection, collectionId: undefined }, ), - ).rejects.toThrow(new WasapValidationError('No collection selected')); + ).rejects.toThrow(WasapValidationError); }); }); @@ -940,13 +938,13 @@ describe('fetchWasapPageData', () => { await expect( fetchWasapPageData(disabledConfig, {}, { mode: WASAP_ANALYSIS_MODE.collection, collectionId: 1 }), - ).rejects.toThrow(new WasapValidationError("Cannot fetch data, 'collection' mode is not enabled.")); + ).rejects.toThrow(WasapValidationError); }); test('throws when no collection is selected', async () => { await expect( fetchWasapPageData(config, {}, { mode: WASAP_ANALYSIS_MODE.collection, collectionId: undefined }), - ).rejects.toThrow(new WasapValidationError('No collection selected')); + ).rejects.toThrow(WasapValidationError); }); }); }); From 9cd1b4864f5de50aa8394fd5fedb05aecbfe8049 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Thu, 30 Jul 2026 10:44:42 +0200 Subject: [PATCH 3/3] Preserve default 3-retry limit for network errors Co-Authored-By: Claude Sonnet 4.6 --- website/src/components/views/wasap/useWasapPageData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/components/views/wasap/useWasapPageData.ts b/website/src/components/views/wasap/useWasapPageData.ts index 20303e434..a2aa86dc4 100644 --- a/website/src/components/views/wasap/useWasapPageData.ts +++ b/website/src/components/views/wasap/useWasapPageData.ts @@ -45,7 +45,7 @@ export function useWasapPageData( return useQuery({ queryKey: ['wasap', analysis, resistanceMutationsBySet], queryFn: () => fetchWasapPageData(config, resistanceMutationsBySet, analysis), - retry: (_, error) => !(error instanceof WasapValidationError), + retry: (failureCount, error) => !(error instanceof WasapValidationError) && failureCount < 3, }); }