diff --git a/libs/shared/ofrep-core/src/lib/api/ofrep-api.spec.ts b/libs/shared/ofrep-core/src/lib/api/ofrep-api.spec.ts index 80bf00612..e4c0ef829 100644 --- a/libs/shared/ofrep-core/src/lib/api/ofrep-api.spec.ts +++ b/libs/shared/ofrep-core/src/lib/api/ofrep-api.spec.ts @@ -24,344 +24,365 @@ describe('OFREPApi', () => { server.listen(); }); beforeEach(() => { - jest.useFakeTimers(); - api = new OFREPApi({ baseUrl: 'https://localhost:8080' }); + api = new OFREPApi({ + baseUrl: 'https://localhost:8080', + // Short timeout to speed up tests + timeoutMs: 500, + }); }); afterEach(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); server.resetHandlers(); }); afterAll(() => { server.close(); }); - describe('postEvaluateFlags should', () => { - it('throw OFREPApiFetchError on network error', async () => { - await expect(() => api.postEvaluateFlag('my-flag', { context: { errors: { network: true } } })).rejects.toThrow( - OFREPApiFetchError, - ); + describe('abort controller', () => { + it('throw OFREPApiFetchError on timed out requests', async () => { + try { + await api.postEvaluateFlag('my-flag', { context: { errors: { slowRequest: true } } }); + } catch (err) { + expect((err as { cause: DOMException })?.cause?.name).toEqual('TimeoutError'); + } }); + }); - it('throw OFREPApiUnexpectedResponseError on any error code without EvaluationFailureResponse body', async () => { - await expect(() => - api.postEvaluateFlag('my-flag', { context: { errors: { generic400: true } } }), - ).rejects.toThrow(OFREPApiUnexpectedResponseError); + describe('mock timers', () => { + beforeEach(() => { + jest.useFakeTimers(); }); - - it('throw OFREPForbiddenError on 401 response', async () => { - await expect(() => api.postEvaluateFlag('my-flag', { context: { errors: { 401: true } } })).rejects.toThrow( - OFREPApiUnauthorizedError, - ); + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); }); - it('throw OFREPForbiddenError on 403 response', async () => { - await expect(() => api.postEvaluateFlag('my-flag', { context: { errors: { 403: true } } })).rejects.toThrow( - OFREPForbiddenError, - ); - }); + describe('postEvaluateFlags should', () => { + it('throw OFREPApiFetchError on network error', async () => { + await expect(() => api.postEvaluateFlag('my-flag', { context: { errors: { network: true } } })).rejects.toThrow( + OFREPApiFetchError, + ); + }); - it('throw OFREPApiTooManyRequestsError on 429 response', async () => { - await expect(() => api.postEvaluateFlag('my-flag', { context: { errors: { 429: true } } })).rejects.toThrow( - OFREPApiTooManyRequestsError, - ); - }); + it('throw OFREPApiUnexpectedResponseError on any error code without EvaluationFailureResponse body', async () => { + await expect(() => + api.postEvaluateFlag('my-flag', { context: { errors: { generic400: true } } }), + ).rejects.toThrow(OFREPApiUnexpectedResponseError); + }); - it('parse numeric Retry-After header correctly on 429 response', async () => { - jest.setSystemTime(new Date('2018-01-27')); + it('throw OFREPForbiddenError on 401 response', async () => { + await expect(() => api.postEvaluateFlag('my-flag', { context: { errors: { 401: true } } })).rejects.toThrow( + OFREPApiUnauthorizedError, + ); + }); - try { - await api.postEvaluateFlag('my-flag', { context: { errors: { 429: true } } }); - } catch (error) { - if (!(error instanceof OFREPApiTooManyRequestsError)) { - throw new Error('Expected OFREPApiTooManyRequestsError'); - } + it('throw OFREPForbiddenError on 403 response', async () => { + await expect(() => api.postEvaluateFlag('my-flag', { context: { errors: { 403: true } } })).rejects.toThrow( + OFREPForbiddenError, + ); + }); - expect(error.retryAfterSeconds).toEqual(2000); - expect(error.retryAfterDate).toEqual(new Date('2018-01-27T00:33:20.000Z')); - } - }); + it('throw OFREPApiTooManyRequestsError on 429 response', async () => { + await expect(() => api.postEvaluateFlag('my-flag', { context: { errors: { 429: true } } })).rejects.toThrow( + OFREPApiTooManyRequestsError, + ); + }); - it('parse date Retry-After header correctly on 429 response', async () => { - jest.setSystemTime(new Date('2018-01-27')); + it('parse numeric Retry-After header correctly on 429 response', async () => { + jest.setSystemTime(new Date('2018-01-27')); - try { - await api.postEvaluateFlag('my-flag', { context: { errors: { 429: 'Sat, 27 Jan 2018 07:28:00 GMT' } } }); - } catch (error) { - if (!(error instanceof OFREPApiTooManyRequestsError)) { - throw new Error('Expected OFREPApiTooManyRequestsError'); + try { + await api.postEvaluateFlag('my-flag', { context: { errors: { 429: true } } }); + } catch (error) { + if (!(error instanceof OFREPApiTooManyRequestsError)) { + throw new Error('Expected OFREPApiTooManyRequestsError'); + } + + expect(error.retryAfterSeconds).toEqual(2000); + expect(error.retryAfterDate).toEqual(new Date('2018-01-27T00:33:20.000Z')); } + }); - expect(error.retryAfterSeconds).toEqual(null); - expect(error.retryAfterDate).toEqual(new Date('2018-01-27T07:28:00.000Z')); - } - }); + it('parse date Retry-After header correctly on 429 response', async () => { + jest.setSystemTime(new Date('2018-01-27')); - it('ignore Retry-After header if it is not valid on 429 response', async () => { - jest.setSystemTime(new Date('2018-01-27')); + try { + await api.postEvaluateFlag('my-flag', { context: { errors: { 429: 'Sat, 27 Jan 2018 07:28:00 GMT' } } }); + } catch (error) { + if (!(error instanceof OFREPApiTooManyRequestsError)) { + throw new Error('Expected OFREPApiTooManyRequestsError'); + } - try { - await api.postEvaluateFlag('my-flag', { context: { errors: { 429: 'abcdefg' } } }); - } catch (error) { - if (!(error instanceof OFREPApiTooManyRequestsError)) { - throw new Error('Expected OFREPApiTooManyRequestsError'); + expect(error.retryAfterSeconds).toEqual(null); + expect(error.retryAfterDate).toEqual(new Date('2018-01-27T07:28:00.000Z')); } + }); - expect(error.retryAfterSeconds).toEqual(null); - expect(error.retryAfterDate).toEqual(null); - } - }); + it('ignore Retry-After header if it is not valid on 429 response', async () => { + jest.setSystemTime(new Date('2018-01-27')); - it('send empty request body if context is not given', async () => { - const result = await api.postEvaluateFlag('my-flag'); - expect(result.httpStatus).toEqual(200); - }); + try { + await api.postEvaluateFlag('my-flag', { context: { errors: { 429: 'abcdefg' } } }); + } catch (error) { + if (!(error instanceof OFREPApiTooManyRequestsError)) { + throw new Error('Expected OFREPApiTooManyRequestsError'); + } - it('send evaluation context in request body', async () => { - const result = await api.postEvaluateFlag('context-in-metadata', { - context: { - targetingKey: 'user-1', - key1: 'value1', - }, + expect(error.retryAfterSeconds).toEqual(null); + expect(error.retryAfterDate).toEqual(null); + } }); - if (result.httpStatus !== 200) { - throw new Error('Received unexpected HTTP status'); - } + it('send empty request body if context is not given', async () => { + const result = await api.postEvaluateFlag('my-flag'); + expect(result.httpStatus).toEqual(200); + }); - expect(result.value.metadata).toEqual({ - context: { - key1: 'value1', - targetingKey: 'user-1', - }, - } satisfies EvaluationContext); - }); + it('send evaluation context in request body', async () => { + const result = await api.postEvaluateFlag('context-in-metadata', { + context: { + targetingKey: 'user-1', + key1: 'value1', + }, + }); - it('return HTTP status in result', async () => { - const result = await api.postEvaluateFlag('my-flag'); - expect(result.httpStatus).toEqual(200); - }); + if (result.httpStatus !== 200) { + throw new Error('Received unexpected HTTP status'); + } - it('return EvaluationFailureResponse response as value on HTTP 400', async () => { - const result = await api.postEvaluateFlag('my-flag', { context: { errors: { notFound: true } } }); - if (result.httpStatus !== 404) { - throw new Error('Received unexpected HTTP status'); - } + expect(result.value.metadata).toEqual({ + context: { + key1: 'value1', + targetingKey: 'user-1', + }, + } satisfies EvaluationContext); + }); - expect(result.value).toEqual({ - key: 'my-flag', - errorCode: EvaluationFailureErrorCode.FlagNotFound, - } satisfies EvaluationFailureResponse); - }); + it('return HTTP status in result', async () => { + const result = await api.postEvaluateFlag('my-flag'); + expect(result.httpStatus).toEqual(200); + }); - it('return EvaluationFailureResponse response as value on HTTP 400', async () => { - const result = await api.postEvaluateFlag('my-flag', { context: { errors: { notFound: true } } }); - if (result.httpStatus !== 404) { - throw new Error('Received unexpected HTTP status'); - } + it('return EvaluationFailureResponse response as value on HTTP 400', async () => { + const result = await api.postEvaluateFlag('my-flag', { context: { errors: { notFound: true } } }); + if (result.httpStatus !== 404) { + throw new Error('Received unexpected HTTP status'); + } - expect(result.value).toEqual({ - key: 'my-flag', - errorCode: EvaluationFailureErrorCode.FlagNotFound, - } satisfies EvaluationFailureResponse); - }); + expect(result.value).toEqual({ + key: 'my-flag', + errorCode: EvaluationFailureErrorCode.FlagNotFound, + } satisfies EvaluationFailureResponse); + }); - it('determine value type based on HTTP status', async () => { - const result = await api.postEvaluateFlag('my-flag'); - expect(result.httpStatus).toEqual(200); + it('return EvaluationFailureResponse response as value on HTTP 400', async () => { + const result = await api.postEvaluateFlag('my-flag', { context: { errors: { notFound: true } } }); + if (result.httpStatus !== 404) { + throw new Error('Received unexpected HTTP status'); + } - // This is to check if the value type is determined by http status code - if (result.httpStatus === 200) { - expect(result.value.value).toBeDefined(); - } else { - expect(result.value.errorCode).toBeDefined(); - } - }); + expect(result.value).toEqual({ + key: 'my-flag', + errorCode: EvaluationFailureErrorCode.FlagNotFound, + } satisfies EvaluationFailureResponse); + }); - it('return EvaluationSuccessResponse response as value on successful evaluation', async () => { - const result = await api.postEvaluateFlag('my-flag', { context: { targetingKey: 'user' } }); - expect(result.httpStatus).toEqual(200); - expect(result.value).toEqual({ - key: 'my-flag', - reason: EvaluationSuccessReason.TargetingMatch, - value: true, - variant: 'default', - metadata: { - context: { - targetingKey: 'user', - }, - }, - } satisfies EvaluationSuccessResponse); - }); + it('determine value type based on HTTP status', async () => { + const result = await api.postEvaluateFlag('my-flag'); + expect(result.httpStatus).toEqual(200); - it('send query params with request', async () => { - api = new OFREPApi({ baseUrl: 'https://localhost:8080', query: new URLSearchParams({ scope: '123' }) }); - const result = await api.postEvaluateFlag('my-flag', { context: { targetingKey: 'user' } }); - expect(result.httpStatus).toEqual(200); - expect(result.value).toEqual({ - key: 'my-flag', - reason: EvaluationSuccessReason.TargetingMatch, - value: true, - variant: 'scoped', - metadata: { - context: { - targetingKey: 'user', - }, - }, - } satisfies EvaluationSuccessResponse); - }); - }); + // This is to check if the value type is determined by http status code + if (result.httpStatus === 200) { + expect(result.value.value).toBeDefined(); + } else { + expect(result.value.errorCode).toBeDefined(); + } + }); - describe('postBulkEvaluateFlags should', () => { - it('throw OFREPApiFetchError on network error', async () => { - await expect(() => api.postBulkEvaluateFlags({ context: { errors: { network: true } } })).rejects.toThrow( - OFREPApiFetchError, - ); - }); + it('return EvaluationSuccessResponse response as value on successful evaluation', async () => { + const result = await api.postEvaluateFlag('my-flag', { context: { targetingKey: 'user' } }); + expect(result.httpStatus).toEqual(200); + expect(result.value).toEqual({ + key: 'my-flag', + reason: EvaluationSuccessReason.TargetingMatch, + value: true, + variant: 'default', + metadata: { + context: { + targetingKey: 'user', + }, + }, + } satisfies EvaluationSuccessResponse); + }); - it('throw OFREPApiUnexpectedResponseError on any error code without EvaluationFailureResponse body', async () => { - await expect(() => api.postBulkEvaluateFlags({ context: { errors: { generic400: true } } })).rejects.toThrow( - OFREPApiUnexpectedResponseError, - ); + it('send query params with request', async () => { + api = new OFREPApi({ baseUrl: 'https://localhost:8080', query: new URLSearchParams({ scope: '123' }) }); + const result = await api.postEvaluateFlag('my-flag', { context: { targetingKey: 'user' } }); + expect(result.httpStatus).toEqual(200); + expect(result.value).toEqual({ + key: 'my-flag', + reason: EvaluationSuccessReason.TargetingMatch, + value: true, + variant: 'scoped', + metadata: { + context: { + targetingKey: 'user', + }, + }, + } satisfies EvaluationSuccessResponse); + }); }); - it('throw OFREPForbiddenError on 401 response', async () => { - await expect(() => api.postBulkEvaluateFlags({ context: { errors: { 401: true } } })).rejects.toThrow( - OFREPApiUnauthorizedError, - ); - }); + describe('postBulkEvaluateFlags should', () => { + it('throw OFREPApiFetchError on network error', async () => { + await expect(() => api.postBulkEvaluateFlags({ context: { errors: { network: true } } })).rejects.toThrow( + OFREPApiFetchError, + ); + }); - it('throw OFREPForbiddenError on 403 response', async () => { - await expect(() => api.postBulkEvaluateFlags({ context: { errors: { 403: true } } })).rejects.toThrow( - OFREPForbiddenError, - ); - }); + it('throw OFREPApiUnexpectedResponseError on any error code without EvaluationFailureResponse body', async () => { + await expect(() => api.postBulkEvaluateFlags({ context: { errors: { generic400: true } } })).rejects.toThrow( + OFREPApiUnexpectedResponseError, + ); + }); - it('throw OFREPApiTooManyRequestsError on 429 response', async () => { - await expect(() => api.postBulkEvaluateFlags({ context: { errors: { 429: true } } })).rejects.toThrow( - OFREPApiTooManyRequestsError, - ); - }); + it('throw OFREPForbiddenError on 401 response', async () => { + await expect(() => api.postBulkEvaluateFlags({ context: { errors: { 401: true } } })).rejects.toThrow( + OFREPApiUnauthorizedError, + ); + }); - it('send empty request body if context is not given', async () => { - const result = await api.postBulkEvaluateFlags(); - expect(result.httpStatus).toEqual(200); - }); + it('throw OFREPForbiddenError on 403 response', async () => { + await expect(() => api.postBulkEvaluateFlags({ context: { errors: { 403: true } } })).rejects.toThrow( + OFREPForbiddenError, + ); + }); - it('send evaluation context in request body', async () => { - const result = await api.postBulkEvaluateFlags({ - context: { - targetingKey: 'user-1', - key1: 'value1', - }, + it('throw OFREPApiTooManyRequestsError on 429 response', async () => { + await expect(() => api.postBulkEvaluateFlags({ context: { errors: { 429: true } } })).rejects.toThrow( + OFREPApiTooManyRequestsError, + ); }); - if (result.httpStatus !== 200) { - throw new Error('Received unexpected HTTP status'); - } + it('send empty request body if context is not given', async () => { + const result = await api.postBulkEvaluateFlags(); + expect(result.httpStatus).toEqual(200); + }); - expect(result.value).toEqual({ - flags: [ - { - key: 'bool-flag', - metadata: { context: { key1: 'value1', targetingKey: 'user-1' } }, - value: true, - reason: EvaluationSuccessReason.Static, - variant: 'variantA', + it('send evaluation context in request body', async () => { + const result = await api.postBulkEvaluateFlags({ + context: { + targetingKey: 'user-1', + key1: 'value1', }, - { - key: 'object-flag', - metadata: { - context: { - key1: 'value1', - targetingKey: 'user-1', - }, + }); + + if (result.httpStatus !== 200) { + throw new Error('Received unexpected HTTP status'); + } + + expect(result.value).toEqual({ + flags: [ + { + key: 'bool-flag', + metadata: { context: { key1: 'value1', targetingKey: 'user-1' } }, + value: true, + reason: EvaluationSuccessReason.Static, + variant: 'variantA', }, - value: { - complex: true, - nested: { - also: true, + { + key: 'object-flag', + metadata: { + context: { + key1: 'value1', + targetingKey: 'user-1', + }, + }, + value: { + complex: true, + nested: { + also: true, + }, }, }, - }, - ], - } satisfies BulkEvaluationSuccessResponse); - }); + ], + } satisfies BulkEvaluationSuccessResponse); + }); - it('return HTTP status in result', async () => { - const result = await api.postBulkEvaluateFlags(); - expect(result.httpStatus).toEqual(200); - }); + it('return HTTP status in result', async () => { + const result = await api.postBulkEvaluateFlags(); + expect(result.httpStatus).toEqual(200); + }); - it('return EvaluationFailureResponse response as value on failed evaluation', async () => { - const result = await api.postBulkEvaluateFlags({ context: { errors: { targetingMissing: true } } }); - if (result.httpStatus !== 400) { - throw new Error('Received unexpected HTTP status'); - } + it('return EvaluationFailureResponse response as value on failed evaluation', async () => { + const result = await api.postBulkEvaluateFlags({ context: { errors: { targetingMissing: true } } }); + if (result.httpStatus !== 400) { + throw new Error('Received unexpected HTTP status'); + } - expect(result.value).toEqual({ - errorCode: EvaluationFailureErrorCode.TargetingKeyMissing, - } satisfies BulkEvaluationFailureResponse); - }); + expect(result.value).toEqual({ + errorCode: EvaluationFailureErrorCode.TargetingKeyMissing, + } satisfies BulkEvaluationFailureResponse); + }); - it('determine value type based on HTTP status', async () => { - const result = await api.postBulkEvaluateFlags(); - expect(result.httpStatus).toEqual(200); + it('determine value type based on HTTP status', async () => { + const result = await api.postBulkEvaluateFlags(); + expect(result.httpStatus).toEqual(200); + + // This is to check if the value type is determined by http status code + if (result.httpStatus === 200) { + expect(result.value.flags).toBeDefined(); + } else if (result.httpStatus === 304) { + expect(result.value).not.toBeDefined(); + } else { + expect(result.value.errorCode).toBeDefined(); + } + }); - // This is to check if the value type is determined by http status code - if (result.httpStatus === 200) { - expect(result.value.flags).toBeDefined(); - } else if (result.httpStatus === 304) { + it('return BulkEvaluationNotModified response as value on 304', async () => { + api = new OFREPApi({ baseUrl: 'https://localhost:8080', headers: [['If-None-Match', '1234']] }); + const result = await api.postBulkEvaluateFlags(undefined); + expect(result.httpStatus).toEqual(304); expect(result.value).not.toBeDefined(); - } else { - expect(result.value.errorCode).toBeDefined(); - } - }); - - it('return BulkEvaluationNotModified response as value on 304', async () => { - api = new OFREPApi({ baseUrl: 'https://localhost:8080', headers: [['If-None-Match', '1234']] }); - const result = await api.postBulkEvaluateFlags(undefined); - expect(result.httpStatus).toEqual(304); - expect(result.value).not.toBeDefined(); - }); + }); - it('send query params with request', async () => { - api = new OFREPApi({ baseUrl: 'https://localhost:8080', query: new URLSearchParams({ scope: '123' }) }); - const result = await api.postBulkEvaluateFlags(); - expect(result.httpStatus).toEqual(200); - expect(result.value).toEqual({ - flags: [ - { - key: 'other-flag', - value: true, - }, - ], + it('send query params with request', async () => { + api = new OFREPApi({ baseUrl: 'https://localhost:8080', query: new URLSearchParams({ scope: '123' }) }); + const result = await api.postBulkEvaluateFlags(); + expect(result.httpStatus).toEqual(200); + expect(result.value).toEqual({ + flags: [ + { + key: 'other-flag', + value: true, + }, + ], + }); }); - }); - it('return BulkEvaluationSuccessResponse response as value on successful evaluation', async () => { - const result = await api.postBulkEvaluateFlags(); - expect(result.httpStatus).toEqual(200); - expect(result.value).toEqual({ - flags: [ - { - key: 'bool-flag', - metadata: {}, - value: true, - reason: EvaluationSuccessReason.Static, - variant: 'variantA', - }, - { - key: 'object-flag', - metadata: {}, - value: { - complex: true, - nested: { - also: true, + it('return BulkEvaluationSuccessResponse response as value on successful evaluation', async () => { + const result = await api.postBulkEvaluateFlags(); + expect(result.httpStatus).toEqual(200); + expect(result.value).toEqual({ + flags: [ + { + key: 'bool-flag', + metadata: {}, + value: true, + reason: EvaluationSuccessReason.Static, + variant: 'variantA', + }, + { + key: 'object-flag', + metadata: {}, + value: { + complex: true, + nested: { + also: true, + }, }, }, - }, - ], + ], + }); }); }); }); diff --git a/libs/shared/ofrep-core/src/lib/api/ofrep-api.ts b/libs/shared/ofrep-core/src/lib/api/ofrep-api.ts index a49a1fd03..1d5ab72cb 100644 --- a/libs/shared/ofrep-core/src/lib/api/ofrep-api.ts +++ b/libs/shared/ofrep-core/src/lib/api/ofrep-api.ts @@ -49,9 +49,10 @@ function isomorphicFetch(): FetchAPI { return fetch; } +const DEFAULT_TIMEOUT_MS = 10_000; + export class OFREPApi { private static readonly jsonRegex = new RegExp(/application\/[^+]*[+]?(json);?.*/, 'i'); - private _etag?: string; constructor( private baseOptions: OFREPProviderBaseOptions, @@ -74,7 +75,16 @@ export class OFREPApi { private async doFetchRequest(req: Request): Promise<{ response: Response; body?: unknown }> { let response: Response; try { - response = await this.fetchImplementation(req); + const timeoutMs = this.baseOptions.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const controller = new AbortController(); + // Uses a setTimeout instead of AbortSignal.timeout to support older runtimes. + setTimeout( + () => controller.abort(new DOMException(`This signal is timeout in ${timeoutMs}ms`, 'TimeoutError')), + timeoutMs, + ); + response = await this.fetchImplementation(req, { + signal: controller.signal, + }); } catch (err) { throw new OFREPApiFetchError(err, 'The OFREP request failed.', { cause: err }); } diff --git a/libs/shared/ofrep-core/src/lib/provider/ofrep-provider-options.ts b/libs/shared/ofrep-core/src/lib/provider/ofrep-provider-options.ts index d1c660baa..457ebfc82 100644 --- a/libs/shared/ofrep-core/src/lib/provider/ofrep-provider-options.ts +++ b/libs/shared/ofrep-core/src/lib/provider/ofrep-provider-options.ts @@ -9,6 +9,12 @@ export type OFREPProviderBaseOptions = { * share the same origin). */ baseUrl: string; + /** + * Abort timeout in milliseconds. + * + * @default 10000 + */ + timeoutMs?: number; /** * Optional fetch implementation */ diff --git a/libs/shared/ofrep-core/src/test/handlers.ts b/libs/shared/ofrep-core/src/test/handlers.ts index db4aaac5b..c21a007ee 100644 --- a/libs/shared/ofrep-core/src/test/handlers.ts +++ b/libs/shared/ofrep-core/src/test/handlers.ts @@ -26,6 +26,11 @@ export const handlers = [ const expectedAuthHeader = requestBody.context?.['expectedAuthHeader'] ?? null; const errors = requestBody.context?.['errors'] as Record | undefined; + if (errors?.['slowRequest']) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + throw HttpResponse.text(undefined, { status: 500 }); + } + if (errors?.['network']) { throw HttpResponse.error(); } @@ -131,6 +136,11 @@ export const handlers = [ const expectedAuthHeader = requestBody.context?.['expectedAuthHeader'] ?? null; const errors = requestBody.context?.['errors'] as Record | undefined; + if (errors?.['slowRequest']) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + throw HttpResponse.text(undefined, { status: 500 }); + } + if (errors?.['network']) { throw HttpResponse.error(); }