Repository: reduxjs/redux-toolkit

Problem statement:
The responseHandler should respect Content-Type headers
Howdy!

I believe the default responseHandler should be a _little_ more intelligent to handle the response based on the content type header.

For example, in the default Go http package, [you can see it is typical to return a response body of "404 page not found"](https://cs.opensource.google/go/go/+/refs/tags/go1.18.2:src/net/http/server.go;l=2101), "Unauthorized," etc. with non-200 responses.

It seems very counterintuitive that one must define a custom `responseHandler` for each and every endpoint to avoid RTK Query failing with `PARSING_ERROR` in some rather common use-cases.


Hints:

Patch:
diff --git a/packages/toolkit/src/query/fetchBaseQuery.ts b/packages/toolkit/src/query/fetchBaseQuery.ts
--- a/packages/toolkit/src/query/fetchBaseQuery.ts
+++ b/packages/toolkit/src/query/fetchBaseQuery.ts
@@ -4,6 +4,7 @@ import type { BaseQueryApi, BaseQueryFn } from './baseQueryTypes'
 import type { MaybePromise, Override } from './tsHelpers'
 
 export type ResponseHandler =
+  | 'content-type'
   | 'json'
   | 'text'
   | ((response: Response) => Promise<any>)
@@ -41,24 +42,6 @@ const defaultValidateStatus = (response: Response) =>
 const defaultIsJsonContentType = (headers: Headers) =>
   /*applicat*/ /ion\/(vnd\.api\+)?json/.test(headers.get('content-type') || '')
 
-const handleResponse = async (
-  response: Response,
-  responseHandler: ResponseHandler
-) => {
-  if (typeof responseHandler === 'function') {
-    return responseHandler(response)
-  }
-
-  if (responseHandler === 'text') {
-    return response.text()
-  }
-
-  if (responseHandler === 'json') {
-    const text = await response.text()
-    return text.length ? JSON.parse(text) : null
-  }
-}
-
 export type FetchBaseQueryError =
   | {
       /**
@@ -342,4 +325,24 @@ export function fetchBaseQuery({
           meta,
         }
   }
+
+  async function handleResponse(
+    response: Response,
+    responseHandler: ResponseHandler
+  ) {
+    if (typeof responseHandler === 'function') {
+      return responseHandler(response)
+    }
+
+    if (responseHandler === 'content-type') {
+      responseHandler = isJsonContentType(response.headers) ? 'json' : 'text'
+    }
+
+    if (responseHandler === 'json') {
+      const text = await response.text()
+      return text.length ? JSON.parse(text) : null
+    }
+
+    return response.text()
+  }
 }


Test patch:
diff --git a/packages/toolkit/src/query/tests/fetchBaseQuery.test.tsx b/packages/toolkit/src/query/tests/fetchBaseQuery.test.tsx
--- a/packages/toolkit/src/query/tests/fetchBaseQuery.test.tsx
+++ b/packages/toolkit/src/query/tests/fetchBaseQuery.test.tsx
@@ -181,6 +181,64 @@ describe('fetchBaseQuery', () => {
       })
     })
 
+    it('success: parse text without error ("content-type" responseHandler)', async () => {
+      server.use(
+        rest.get('https://example.com/success', (_, res, ctx) =>
+          res.once(
+            ctx.text(`this is not json!`)
+            // NOTE: MSW sets content-type header as text automatically
+          )
+        )
+      )
+
+      const req = baseQuery(
+        {
+          url: '/success',
+          responseHandler: 'content-type',
+        },
+        commonBaseQueryApi,
+        {}
+      )
+      expect(req).toBeInstanceOf(Promise)
+      const res = await req
+      expect(res).toBeInstanceOf(Object)
+      expect(res.meta?.response?.headers.get('content-type')).toEqual(
+        'text/plain'
+      )
+      expect(res.meta?.request).toBeInstanceOf(Request)
+      expect(res.meta?.response).toBeInstanceOf(Object)
+      expect(res.data).toEqual(`this is not json!`)
+    })
+
+    it('success: parse json without error ("content-type" responseHandler)', async () => {
+      server.use(
+        rest.get('https://example.com/success', (_, res, ctx) =>
+          res.once(
+            ctx.json(`this will become json!`)
+            // NOTE: MSW sets content-type header as json automatically
+          )
+        )
+      )
+
+      const req = baseQuery(
+        {
+          url: '/success',
+          responseHandler: 'content-type',
+        },
+        commonBaseQueryApi,
+        {}
+      )
+      expect(req).toBeInstanceOf(Promise)
+      const res = await req
+      expect(res).toBeInstanceOf(Object)
+      expect(res.meta?.response?.headers.get('content-type')).toEqual(
+        'application/json'
+      )
+      expect(res.meta?.request).toBeInstanceOf(Request)
+      expect(res.meta?.response).toBeInstanceOf(Object)
+      expect(res.data).toEqual(`this will become json!`)
+    })
+
     it('server error: should fail normally with a 500 status ("text" responseHandler)', async () => {
       server.use(
         rest.get('https://example.com/error', (_, res, ctx) =>
@@ -204,6 +262,62 @@ describe('fetchBaseQuery', () => {
       })
     })
 
+    it('server error: should fail normally with a 500 status as text ("content-type" responseHandler)', async () => {
+      const serverResponse = 'Internal Server Error'
+      server.use(
+        rest.get('https://example.com/error', (_, res, ctx) =>
+          res(ctx.status(500), ctx.text(serverResponse))
+        )
+      )
+
+      const req = baseQuery(
+        { url: '/error', responseHandler: 'content-type' },
+        commonBaseQueryApi,
+        {}
+      )
+      expect(req).toBeInstanceOf(Promise)
+      const res = await req
+      expect(res).toBeInstanceOf(Object)
+      expect(res.meta?.request).toBeInstanceOf(Request)
+      expect(res.meta?.response).toBeInstanceOf(Object)
+      expect(res.meta?.response?.headers.get('content-type')).toEqual(
+        'text/plain'
+      )
+      expect(res.error).toEqual({
+        status: 500,
+        data: serverResponse,
+      })
+    })
+
+    it('server error: should fail normally with a 500 status as json ("content-type" responseHandler)', async () => {
+      const serverResponse = {
+        errors: { field1: "Password cannot be 'password'" },
+      }
+      server.use(
+        rest.get('https://example.com/error', (_, res, ctx) =>
+          res(ctx.status(500), ctx.json(serverResponse))
+        )
+      )
+
+      const req = baseQuery(
+        { url: '/error', responseHandler: 'content-type' },
+        commonBaseQueryApi,
+        {}
+      )
+      expect(req).toBeInstanceOf(Promise)
+      const res = await req
+      expect(res).toBeInstanceOf(Object)
+      expect(res.meta?.request).toBeInstanceOf(Request)
+      expect(res.meta?.response).toBeInstanceOf(Object)
+      expect(res.meta?.response?.headers.get('content-type')).toEqual(
+        'application/json'
+      )
+      expect(res.error).toEqual({
+        status: 500,
+        data: serverResponse,
+      })
+    })
+
     it('server error: should fail gracefully (default="json" responseHandler)', async () => {
       server.use(
         rest.get('https://example.com/error', (_, res, ctx) =>