From c887f6c690ab803b48693dee55ef30dd0343c79d Mon Sep 17 00:00:00 2001 From: Roman Usherenko Date: Fri, 17 Dec 2021 00:31:57 +0200 Subject: [PATCH 1/2] add transformErrorReponse to rtkq endpoints --- docs/rtk-query/api/createApi.mdx | 29 +++++++++++ docs/rtk-query/usage-with-typescript.mdx | 2 +- docs/rtk-query/usage/customizing-queries.mdx | 51 +++++++++++++++++++ docs/rtk-query/usage/mutations.mdx | 2 + docs/rtk-query/usage/queries.mdx | 2 + .../core/buildMiddleware/queryLifecycle.ts | 2 +- .../toolkit/src/query/core/buildThunks.ts | 27 ++++++++-- .../toolkit/src/query/endpointDefinitions.ts | 13 +++++ .../toolkit/src/query/tests/createApi.test.ts | 19 +++++++ .../src/query/tests/devWarnings.test.tsx | 25 +++++++++ .../toolkit/src/query/tests/queryFn.test.tsx | 16 ++++-- 11 files changed, 178 insertions(+), 10 deletions(-) diff --git a/docs/rtk-query/api/createApi.mdx b/docs/rtk-query/api/createApi.mdx index f2e60917f7..fa8c56279b 100644 --- a/docs/rtk-query/api/createApi.mdx +++ b/docs/rtk-query/api/createApi.mdx @@ -158,6 +158,13 @@ export type QueryDefinition< arg: QueryArg ): ResultType | Promise + /* transformErrorResponse only available with `query`, not `queryFn` */ + transformErrorResponse?( + baseQueryReturnValue: BaseQueryError, + meta: BaseQueryMeta, + arg: QueryArg + ): unknown + extraOptions?: BaseQueryExtraOptions providesTags?: ResultDescription< @@ -226,6 +233,13 @@ export type MutationDefinition< arg: QueryArg ): ResultType | Promise + /* transformErrorResponse only available with `query`, not `queryFn` */ + transformErrorResponse?( + baseQueryReturnValue: BaseQueryError, + meta: BaseQueryMeta, + arg: QueryArg + ): unknown + extraOptions?: BaseQueryExtraOptions invalidatesTags?: ResultDescription @@ -431,6 +445,21 @@ transformResponse: (response, meta, arg) => response.some.deeply.nested.collection ``` +### `transformErrorResponse` + +_(optional, not applicable with `queryFn`)_ + +[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.transformErrorResponse) + +In some cases, you may want to manipulate the error returned from a query before you put it in the cache. In this instance, you can take advantage of `transformErrorResponse`. + +See also [Customizing query responses with `transformErrorResponse`](../usage/customizing-queries.mdx#customizing-query-responses-with-transformerrorresponse) + +```ts title="Unpack a deeply nested error object" no-transpile +transformErrorResponse: (response, meta, arg) => + response.data.some.deeply.nested.errorObject +``` + ### `extraOptions` _(optional)_ diff --git a/docs/rtk-query/usage-with-typescript.mdx b/docs/rtk-query/usage-with-typescript.mdx index d7df90a583..aef8eaba41 100644 --- a/docs/rtk-query/usage-with-typescript.mdx +++ b/docs/rtk-query/usage-with-typescript.mdx @@ -135,7 +135,7 @@ The `BaseQueryFn` type accepts the following generics: - `Result` - The type to be returned in the `data` property for the success case. Unless you expect all queries and mutations to return the same type, it is recommended to keep this typed as `unknown`, and specify the types individually as shown [below](#typing-query-and-mutation-endpoints). - `Error` - The type to be returned for the `error` property in the error case. This type also applies to all [`queryFn`](#typing-a-queryfn) functions used in endpoints throughout the API definition. - `DefinitionExtraOptions` - The type for the third parameter of the function. The value provided to the [`extraOptions`](./api/createApi.mdx#extraoptions) property on an endpoint will be passed here. -- `Meta` - the type of the `meta` property that may be returned from calling the `baseQuery`. The `meta` property is accessible as the second argument to [`transformResponse`](./api/createApi.mdx#transformresponse). +- `Meta` - the type of the `meta` property that may be returned from calling the `baseQuery`. The `meta` property is accessible as the second argument to [`transformResponse`](./api/createApi.mdx#transformresponse) and [`transformErrorResponse`](./api/createApi.mdx#transformerrorresponse). :::note diff --git a/docs/rtk-query/usage/customizing-queries.mdx b/docs/rtk-query/usage/customizing-queries.mdx index 62bca29419..25621cd684 100644 --- a/docs/rtk-query/usage/customizing-queries.mdx +++ b/docs/rtk-query/usage/customizing-queries.mdx @@ -175,6 +175,57 @@ transformResponse: (response) => See also [Websocket Chat API with a transformed response shape](./streaming-updates.mdx#websocket-chat-api-with-a-transformed-response-shape) for an example of `transformResponse` normalizing response data in combination with `createEntityAdapter`, while also updating further data using [`streaming updates`](./streaming-updates.mdx). +## Customizing query responses with `transformErrorResponse` + +Individual endpoints on [`createApi`](../api/createApi.mdx) accept a [`transformErrorResponse`](../api/createApi.mdx) property which allows manipulation of the errir returned by a query or mutation before it hits the cache. + +`transformErrorResponse` is called with the error that a failed `baseQuery` returns for the corresponding endpoint, and the return value of `transformErrorResponse` is used as the cached error associated with that endpoint call. + +By default, the payload from the server is returned directly. + +```ts +function defaultTransformResponse( + baseQueryReturnValue: unknown, + meta: unknown, + arg: unknown +) { + return baseQueryReturnValue +} +``` + +To change it, provide a function that looks like: + +```ts title="Unpack a deeply nested error object" no-transpile +transformErrorResponse: (response, meta, arg) => + response.data.some.deeply.nested.errorObject +``` + +`transformErrorResponse` is called with the `meta` property returned from the `baseQuery` as its second +argument, which can be used while determining the transformed response. The value for `meta` is +dependent on the `baseQuery` used. + +```ts title="transformErrorResponse meta example" no-transpile +transformErrorResponse: (response: { data: { sideA: Tracks; sideB: Tracks } }, meta, arg) => { + if (meta?.coinFlip === 'heads') { + return response.data.sideA + } + return response.data.sideB +} +``` + +`transformErrorResponse` is called with the `arg` property provided to the endpoint as its third +argument, which can be used while determining the transformed response. The value for `arg` is +dependent on the `endpoint` used, as well as the argument used when calling the query/mutation. + +```ts title="transformErrorResponse arg example" no-transpile +transformErrorResponse: (response: Posts, meta, arg) => { + return { + originalArg: arg, + error: response, + } +} +``` + ## Customizing queries with `queryFn` Individual endpoints on [`createApi`](../api/createApi.mdx) accept a [`queryFn`](../api/createApi.mdx#queryfn) property which allows a given endpoint to ignore `baseQuery` for that endpoint by providing an inline function determining how that query resolves. diff --git a/docs/rtk-query/usage/mutations.mdx b/docs/rtk-query/usage/mutations.mdx index 6459d2e1c1..d820f738e7 100644 --- a/docs/rtk-query/usage/mutations.mdx +++ b/docs/rtk-query/usage/mutations.mdx @@ -51,6 +51,8 @@ const api = createApi({ }), // Pick out data and prevent nested properties in a hook or selector transformResponse: (response: { data: Post }, meta, arg) => response.data, + // Pick out errors and prevent nested properties in a hook or selector + transformErrorResponse: (response: { status: string | number }, meta, arg) => response.status, invalidatesTags: ['Post'], // onQueryStarted is useful for optimistic updates // The 2nd parameter is the destructured `MutationLifecycleApi` diff --git a/docs/rtk-query/usage/queries.mdx b/docs/rtk-query/usage/queries.mdx index dbf27a45d5..6db0f65aea 100644 --- a/docs/rtk-query/usage/queries.mdx +++ b/docs/rtk-query/usage/queries.mdx @@ -58,6 +58,8 @@ const api = createApi({ query: (id) => ({ url: `post/${id}` }), // Pick out data and prevent nested properties in a hook or selector transformResponse: (response: { data: Post }, meta, arg) => response.data, + // Pick out errors and prevent nested properties in a hook or selector + transformErrorResponse: (response: { status: string | number }, meta, arg) => response.status, providesTags: (result, error, id) => [{ type: 'Post', id }], // The 2nd parameter is the destructured `QueryLifecycleApi` async onQueryStarted( diff --git a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts index 43d3601798..c89a29f8d8 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts @@ -60,7 +60,7 @@ declare module '../../endpointDefinitions' { error: unknown meta?: undefined /** - * If this is `true`, that means that this error is the result of `baseQueryFn`, `queryFn` or `transformResponse` throwing an error instead of handling it properly. + * If this is `true`, that means that this error is the result of `baseQueryFn`, `queryFn`, `transformResponse` or `transformErrorResponse` throwing an error instead of handling it properly. * There can not be made any assumption about the shape of `error`. */ isUnhandledError: true diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 0bfec01389..7ecbca79b7 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -353,8 +353,25 @@ export function buildThunks< } ) } catch (error) { - if (error instanceof HandledError) { - return rejectWithValue(error.value, { baseQueryMeta: error.meta }) + let catchedError = error + if (catchedError instanceof HandledError) { + let transformErrorResponse: ( + baseQueryReturnValue: any, + meta: any, + arg: any + ) => any = defaultTransformResponse + + if (endpointDefinition.query && endpointDefinition.transformErrorResponse) { + transformErrorResponse = endpointDefinition.transformErrorResponse + } + try { + return rejectWithValue( + await transformErrorResponse(catchedError.value, catchedError.meta, arg.originalArgs), + { baseQueryMeta: catchedError.meta } + ) + } catch (e) { + catchedError = e + } } if ( typeof process !== 'undefined' && @@ -363,12 +380,12 @@ export function buildThunks< console.error( `An unhandled error occurred processing a request for the endpoint "${arg.endpointName}". In the case of an unhandled error, no tags will be "provided" or "invalidated".`, - error + catchedError ) } else { - console.error(error) + console.error(catchedError) } - throw error + throw catchedError } } diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index e8f2a1f626..0bbdcc867c 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -64,6 +64,14 @@ interface EndpointDefinitionWithQuery< meta: BaseQueryMeta, arg: QueryArg ): ResultType | Promise + /** + * A function to manipulate the data returned by a failed query or mutation. + */ + transformErrorResponse?( + baseQueryReturnValue: BaseQueryError, + meta: BaseQueryMeta, + arg: QueryArg + ): unknown /** * Defaults to `true`. * @@ -130,6 +138,7 @@ interface EndpointDefinitionWithQueryFn< ): MaybePromise>> query?: never transformResponse?: never + transformErrorResponse?: never /** * Defaults to `true`. * @@ -425,6 +434,8 @@ export type EndpointBuilder< * query: (id) => ({ url: `post/${id}` }), * // Pick out data and prevent nested properties in a hook or selector * transformResponse: (response) => response.data, + * // Pick out error and prevent nested properties in a hook or selector + * transformErrorResponse: (response) => response.error, * // `result` is the server response * providesTags: (result, error, id) => [{ type: 'Post', id }], * // trigger side effects or optimistic updates @@ -455,6 +466,8 @@ export type EndpointBuilder< * query: ({ id, ...patch }) => ({ url: `post/${id}`, method: 'PATCH', body: patch }), * // Pick out data and prevent nested properties in a hook or selector * transformResponse: (response) => response.data, + * // Pick out error and prevent nested properties in a hook or selector + * transformErrorResponse: (response) => response.error, * // `result` is the server response * invalidatesTags: (result, error, id) => [{ type: 'Post', id }], * // trigger side effects or optimistic updates diff --git a/packages/toolkit/src/query/tests/createApi.test.ts b/packages/toolkit/src/query/tests/createApi.test.ts index fe8085348e..ed0c0e9cb7 100644 --- a/packages/toolkit/src/query/tests/createApi.test.ts +++ b/packages/toolkit/src/query/tests/createApi.test.ts @@ -516,6 +516,7 @@ describe('endpoint definition typings', () => { describe('additional transformResponse behaviors', () => { type SuccessResponse = { value: 'success' } type EchoResponseData = { banana: 'bread' } + type ErrorResponse = { value: 'error' } const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), endpoints: (build) => ({ @@ -531,6 +532,16 @@ describe('additional transformResponse behaviors', () => { transformResponse: (response: { body: { nested: EchoResponseData } }) => response.body.nested, }), + mutationWithError: build.mutation({ + query: () => ({ + url: '/error', + method: 'POST', + }), + transformErrorResponse: (response) => { + const data = response.data as ErrorResponse + return data.value + }, + }), mutationWithMeta: build.mutation({ query: () => ({ url: '/echo', @@ -596,6 +607,14 @@ describe('additional transformResponse behaviors', () => { expect('data' in result && result.data).toEqual({ banana: 'bread' }) }) + test('transformResponse transforms a response from a mutation with an error', async () => { + const result = await storeRef.store.dispatch( + api.endpoints.mutationWithError.initiate({}) + ) + + expect('error' in result && result.error).toEqual('error') + }) + test('transformResponse can inject baseQuery meta into the end result from a mutation', async () => { const result = await storeRef.store.dispatch( api.endpoints.mutationWithMeta.initiate({}) diff --git a/packages/toolkit/src/query/tests/devWarnings.test.tsx b/packages/toolkit/src/query/tests/devWarnings.test.tsx index ad581be776..c52f46dea6 100644 --- a/packages/toolkit/src/query/tests/devWarnings.test.tsx +++ b/packages/toolkit/src/query/tests/devWarnings.test.tsx @@ -340,6 +340,31 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated". In the case of an unhandled error, no tags will be "provided" or "invalidated". [Error: this was kinda expected]`) }) + test('error thrown in `transformErrorResponse`', async () => { + const api = createApi({ + baseQuery() { + return { error: {} } + }, + endpoints: (build) => ({ + transformErRspn: build.query({ + query() {}, + transformErrorResponse() { + throw new Error('this was kinda expected') + }, + }), + }), + }) + const store = configureStore({ + reducer: { [api.reducerPath]: api.reducer }, + middleware: (gdm) => gdm().concat(api.middleware), + }) + await store.dispatch(api.endpoints.transformErRspn.initiate()) + + expect(getLog().log) + .toBe(`An unhandled error occurred processing a request for the endpoint "transformErRspn". +In the case of an unhandled error, no tags will be "provided" or "invalidated". [Error: this was kinda expected]`) + }) + test('`fetchBaseQuery`: error thrown in `prepareHeaders`', async () => { const api = createApi({ baseQuery: fetchBaseQuery({ diff --git a/packages/toolkit/src/query/tests/queryFn.test.tsx b/packages/toolkit/src/query/tests/queryFn.test.tsx index 998dc589a3..1562153aff 100644 --- a/packages/toolkit/src/query/tests/queryFn.test.tsx +++ b/packages/toolkit/src/query/tests/queryFn.test.tsx @@ -9,9 +9,9 @@ import type { QuerySubState } from '@reduxjs/toolkit/dist/query/core/apiState' describe('queryFn base implementation tests', () => { const baseQuery: BaseQueryFn = - jest.fn((arg: string) => ({ - data: { wrappedByBaseQuery: arg }, - })) + jest.fn((arg: string) => arg.includes('withErrorQuery') + ? ({ error: `cut${arg}` }) + : ({ data: { wrappedByBaseQuery: arg } })) const api = createApi({ baseQuery, @@ -24,6 +24,14 @@ describe('queryFn base implementation tests', () => { return response.wrappedByBaseQuery }, }), + withErrorQuery: build.query({ + query(arg: string) { + return `resultFrom(${arg})` + }, + transformErrorResponse(response) { + return response.slice(3) + }, + }), withQueryFn: build.query({ queryFn(arg: string) { return { data: `resultFrom(${arg})` } @@ -141,6 +149,7 @@ describe('queryFn base implementation tests', () => { const { withQuery, + withErrorQuery, withQueryFn, withErrorQueryFn, withThrowingQueryFn, @@ -166,6 +175,7 @@ describe('queryFn base implementation tests', () => { test.each([ ['withQuery', withQuery, 'data'], + ['withErrorQuery', withErrorQuery, 'error'], ['withQueryFn', withQueryFn, 'data'], ['withErrorQueryFn', withErrorQueryFn, 'error'], ['withThrowingQueryFn', withThrowingQueryFn, 'throw'], From 48f52e092b97209878f3604af2113c2a3154125b Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 15 Aug 2022 21:18:35 -0400 Subject: [PATCH 2/2] Remove time-sensitive CI test --- .../toolkit/src/query/core/buildThunks.ts | 21 ++++++++++++------- .../toolkit/src/query/tests/cleanup.test.tsx | 9 -------- .../src/query/tests/devWarnings.test.tsx | 7 +++++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 7ecbca79b7..e873bde369 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -356,18 +356,25 @@ export function buildThunks< let catchedError = error if (catchedError instanceof HandledError) { let transformErrorResponse: ( - baseQueryReturnValue: any, - meta: any, - arg: any + baseQueryReturnValue: any, + meta: any, + arg: any ) => any = defaultTransformResponse - if (endpointDefinition.query && endpointDefinition.transformErrorResponse) { + if ( + endpointDefinition.query && + endpointDefinition.transformErrorResponse + ) { transformErrorResponse = endpointDefinition.transformErrorResponse } try { return rejectWithValue( - await transformErrorResponse(catchedError.value, catchedError.meta, arg.originalArgs), - { baseQueryMeta: catchedError.meta } + await transformErrorResponse( + catchedError.value, + catchedError.meta, + arg.originalArgs + ), + { baseQueryMeta: catchedError.meta } ) } catch (e) { catchedError = e @@ -375,7 +382,7 @@ export function buildThunks< } if ( typeof process !== 'undefined' && - process.env.NODE_ENV === 'development' + process.env.NODE_ENV !== 'production' ) { console.error( `An unhandled error occurred processing a request for the endpoint "${arg.endpointName}". diff --git a/packages/toolkit/src/query/tests/cleanup.test.tsx b/packages/toolkit/src/query/tests/cleanup.test.tsx index 5aebc49758..e9b9b95992 100644 --- a/packages/toolkit/src/query/tests/cleanup.test.tsx +++ b/packages/toolkit/src/query/tests/cleanup.test.tsx @@ -177,8 +177,6 @@ test('Minimizes the number of subscription dispatches when multiple components a return <>{listItems} } - const start = Date.now() - render(, { wrapper: storeRef.wrapper, }) @@ -189,10 +187,6 @@ test('Minimizes the number of subscription dispatches when multiple components a return screen.getAllByText(/42/).length > 0 }) - const end = Date.now() - - const timeElapsed = end - start - const subscriptions = getSubscriptionsA() expect(Object.keys(subscriptions!).length).toBe(NUM_LIST_ITEMS) @@ -203,7 +197,4 @@ test('Minimizes the number of subscription dispatches when multiple components a // 'api/executeQuery/fulfilled' // ] expect(actionTypes.length).toBe(4) - // Could be flaky in CI, but we'll see. - // Currently seeing 1000ms in local dev, 6300 without the batching fixes - expect(timeElapsed).toBeLessThan(2500) }, 25000) diff --git a/packages/toolkit/src/query/tests/devWarnings.test.tsx b/packages/toolkit/src/query/tests/devWarnings.test.tsx index c52f46dea6..5dbd22a6fa 100644 --- a/packages/toolkit/src/query/tests/devWarnings.test.tsx +++ b/packages/toolkit/src/query/tests/devWarnings.test.tsx @@ -346,8 +346,11 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated". return { error: {} } }, endpoints: (build) => ({ - transformErRspn: build.query({ - query() {}, + // @ts-ignore TS doesn't like `() => never` for `tER` + transformErRspn: build.query({ + // @ts-ignore TS doesn't like `() => never` for `tER` + query: () => '/dummy', + // @ts-ignore TS doesn't like `() => never` for `tER` transformErrorResponse() { throw new Error('this was kinda expected') },