diff --git a/src/search/BaseSearchSource.ts b/src/search/BaseSearchSource.ts index d018ef0ae..d8907e546 100644 --- a/src/search/BaseSearchSource.ts +++ b/src/search/BaseSearchSource.ts @@ -6,6 +6,8 @@ import type { SearchSourceState, SearchSourceType, } from './types'; +import type { APIError } from '../errors'; +import { isAPIError, isErrorRetryable } from '../errors'; export type DebounceOptions = { debounceMs: number; @@ -237,6 +239,9 @@ export abstract class BaseSearchSource stateUpdate.items = await this.filterQueryResults(items); } catch (e) { stateUpdate.lastQueryError = e as Error; + if (isAPIError(e as Error) && !isErrorRetryable(e as APIError)) { + stateUpdate.hasNext = false; + } } finally { this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery)); } @@ -285,6 +290,9 @@ export abstract class BaseSearchSourceSync stateUpdate.items = this.filterQueryResults(items); } catch (e) { stateUpdate.lastQueryError = e as Error; + if (isAPIError(e as Error) && !isErrorRetryable(e as APIError)) { + stateUpdate.hasNext = false; + } } finally { this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery)); } diff --git a/test/unit/search/SearchController.test.js b/test/unit/search/SearchController.test.js index d9671cdda..6d75704cf 100644 --- a/test/unit/search/SearchController.test.js +++ b/test/unit/search/SearchController.test.js @@ -10,7 +10,9 @@ import { import { generateUser } from '../test-utils/generateUser'; import { generateChannel } from '../test-utils/generateChannel'; -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { ErrorFromResponse } from '../../../src'; +import { APIErrorCodes } from '../../../src/errors'; describe('SearchController', () => { let searchController; @@ -407,6 +409,42 @@ describe('BaseSearchSource and implementations', () => { expect(searchSource.items).to.deep.equal(['item1']); expect(searchSource.hasNext).to.be.false; }); + + it('stores error from a failed request, keeps ability to paginate', async () => { + searchSource.pageSize = 1; + searchSource.state.partialNext({ + items: [], + isActive: true, + searchQuery: 'test', + }); + + vi.spyOn(searchSource, 'query').mockRejectedValue(new Error('anything')); + + await searchSource.executeQuery(); + expect(searchSource.items).toStrictEqual([]); + expect(searchSource.hasNext).toBe(true); + }); + + it('terminates pagination on non-retryable error', async () => { + searchSource.pageSize = 1; + searchSource.state.partialNext({ + items: [], + isActive: true, + searchQuery: 'test', + }); + + vi.spyOn(searchSource, 'query').mockRejectedValue( + new ErrorFromResponse('anything', { + code: APIErrorCodes[4], + response: {}, + status: 400, + }), + ); + + await searchSource.executeQuery(); + expect(searchSource.items).toStrictEqual([]); + expect(searchSource.hasNext).toBe(false); + }); }); describe('search debounce', () => {