diff --git a/src/http-requests.ts b/src/http-requests.ts index cf0d5470f..25fd97852 100644 --- a/src/http-requests.ts +++ b/src/http-requests.ts @@ -91,11 +91,13 @@ class HttpRequests { url: URL requestConfig?: Config['requestConfig'] httpClient?: Required['httpClient'] + requestTimeout?: number constructor(config: Config) { this.headers = createHeaders(config) this.requestConfig = config.requestConfig this.httpClient = config.httpClient + this.requestTimeout = config.timeout try { const host = constructHostURL(config.host) @@ -140,14 +142,17 @@ class HttpRequests { const headers = { ...this.headers, ...config.headers } try { - const fetchFn = this.httpClient ? this.httpClient : fetch - const result = fetchFn(constructURL.toString(), { - ...config, - ...this.requestConfig, - method, - body, - headers, - }) + const result = this.fetchWithTimeout( + constructURL.toString(), + { + ...config, + ...this.requestConfig, + method, + body, + headers, + }, + this.requestTimeout + ) // When using a custom HTTP client, the response is returned to allow the user to parse/handle it as they see fit if (this.httpClient) { @@ -166,6 +171,39 @@ class HttpRequests { } } + async fetchWithTimeout( + url: string, + options: Record | RequestInit | undefined, + timeout: HttpRequests['requestTimeout'] + ): Promise { + return new Promise((resolve, reject) => { + const fetchFn = this.httpClient ? this.httpClient : fetch + + const fetchPromise = fetchFn(url, options) + + const promises: Array> = [fetchPromise] + + // TimeoutPromise will not run if undefined or zero + let timeoutId: ReturnType + if (timeout) { + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new Error('Error: Request Timed Out')) + }, timeout) + }) + + promises.push(timeoutPromise) + } + + Promise.race(promises) + .then(resolve) + .catch(reject) + .finally(() => { + clearTimeout(timeoutId) + }) + }) + } + async get( url: string, params?: { [key: string]: any }, diff --git a/src/types/types.ts b/src/types/types.ts index be1a6b8fc..e4f3b5182 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -12,6 +12,7 @@ export type Config = { clientAgents?: string[] requestConfig?: Partial> httpClient?: (input: string, init?: RequestInit) => Promise + timeout?: number } /// diff --git a/tests/search.test.ts b/tests/search.test.ts index 222f36323..21246f9fc 100644 --- a/tests/search.test.ts +++ b/tests/search.test.ts @@ -1059,6 +1059,21 @@ describe.each([ expect(error).toHaveProperty('message', 'The user aborted a request.') }) }) + + test(`${permission} key: search should be aborted when reaching timeout`, async () => { + const key = await getKey(permission) + const client = new MeiliSearch({ + ...config, + apiKey: key, + timeout: 1, + }) + try { + await client.health() + } catch (e: any) { + expect(e.message).toEqual('Error: Request Timed Out') + expect(e.name).toEqual('MeiliSearchCommunicationError') + } + }) }) describe.each([