diff --git a/docs/rtk-query/api/createApi.mdx b/docs/rtk-query/api/createApi.mdx index f2e60917f7..55245210f6 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: BaseQueryResult, + meta: BaseQueryMeta, + arg: QueryArg + ): ResultType | Promise + extraOptions?: BaseQueryExtraOptions providesTags?: ResultDescription< @@ -226,6 +233,13 @@ export type MutationDefinition< arg: QueryArg ): ResultType | Promise + /* transformErrorResponse only available with `query`, not `queryFn` */ + transformErrorResponse?( + baseQueryReturnValue: BaseQueryResult, + meta: BaseQueryMeta, + arg: QueryArg + ): ResultType | Promise + 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.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..4f52becf87 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 +transformResponse: (response, meta, arg) => + response.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: { sideA: Tracks; sideB: Tracks }, meta, arg) => { + if (meta?.coinFlip === 'heads') { + return response.sideA + } + return response.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..4d9df85ebb 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: { error: unknown }, meta, arg) => response.error, 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..257c7a8596 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: { error: unknown }, meta, arg) => response.error, 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 53c6c4305f..ce3a3fe24f 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -344,8 +344,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' && @@ -354,12 +371,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 44ce1a41b1..450f995607 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -62,6 +62,14 @@ interface EndpointDefinitionWithQuery< baseQueryReturnValue: BaseQueryResult, 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 ): ResultType | Promise /** * Defaults to `true`. @@ -129,6 +137,7 @@ interface EndpointDefinitionWithQueryFn< ): MaybePromise>> query?: never transformResponse?: never + transformErrorResponse?: never /** * Defaults to `true`. * @@ -403,6 +412,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 @@ -433,6 +444,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 fbf729841a..cc4c8af8e0 100644 --- a/packages/toolkit/src/query/tests/createApi.test.ts +++ b/packages/toolkit/src/query/tests/createApi.test.ts @@ -510,6 +510,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) => ({ @@ -525,6 +526,14 @@ describe('additional transformResponse behaviors', () => { transformResponse: (response: { body: { nested: EchoResponseData } }) => response.body.nested, }), + mutationWithError: build.mutation({ + query: () => ({ + url: '/error', + method: 'POST' + }), + transformErrorResponse: (response: { data: ErrorResponse }) => + response.data.value, + }), mutationWithMeta: build.mutation({ query: () => ({ url: '/echo', @@ -590,6 +599,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'],