Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(core): Handle text/* type fetch responses (#2456)

* handle text responses

* update tests

* fix tests

* Replace error fallback with text/* special content handling

The outer `catch` was still responsible for handling errors,
so we can avoid duplication here and still use `statusText`.
We should instead add a separate case for `text/*` contents.
Those should never really be returned by endpoints, but if they
are we can return them as it was in this branch before.

* Update multipart snapshots

Co-authored-by: jdecroock <decroockjovi@gmail.com>

authored by

Phil Pluckthun
jdecroock
and committed by
GitHub
2695fb93 cabbcd7e

+98 -62
+5
.changeset/moody-brooms-refuse.md
··· 1 + --- 2 + '@urql/core': patch 3 + --- 4 + 5 + Passthrough responses with content type of `text/*` as error messages.
+3 -1
exchanges/execute/src/execute.test.ts
··· 167 167 168 168 fetchMock.mockResolvedValue({ 169 169 status: 200, 170 - json: jest.fn().mockResolvedValue({ data: mockHttpResponseData }), 170 + text: jest 171 + .fn() 172 + .mockResolvedValue(JSON.stringify({ data: mockHttpResponseData })), 171 173 }); 172 174 173 175 const responseFromFetchExchange = await pipe(
+2 -2
exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
··· 3 3 exports[`on error returns error data 1`] = ` 4 4 Object { 5 5 "data": undefined, 6 - "error": [CombinedError: [Network] ], 6 + "error": [CombinedError: [Network] No Content], 7 7 "extensions": undefined, 8 8 "operation": Object { 9 9 "context": Object { ··· 142 142 exports[`on error returns error data with status 400 and manual redirect mode 1`] = ` 143 143 Object { 144 144 "data": undefined, 145 - "error": [CombinedError: [Network] ], 145 + "error": [CombinedError: [Network] No Content], 146 146 "extensions": undefined, 147 147 "operation": Object { 148 148 "context": Object {
+6 -6
exchanges/multipart-fetch/src/multipartFetchExchange.test.ts
··· 31 31 (global as any).AbortController = undefined; 32 32 }); 33 33 34 - const response = { 34 + const response = JSON.stringify({ 35 35 status: 200, 36 36 data: { 37 37 data: { 38 38 user: 1200, 39 39 }, 40 40 }, 41 - }; 41 + }); 42 42 43 43 const exchangeArgs = { 44 44 forward: () => empty as Source<OperationResult>, ··· 50 50 beforeEach(() => { 51 51 fetch.mockResolvedValue({ 52 52 status: 200, 53 - json: jest.fn().mockResolvedValue(response), 53 + text: jest.fn().mockResolvedValue(response), 54 54 }); 55 55 }); 56 56 ··· 121 121 beforeEach(() => { 122 122 fetch.mockResolvedValue({ 123 123 status: 400, 124 - json: jest.fn().mockResolvedValue({}), 124 + text: jest.fn().mockResolvedValue('{}'), 125 125 }); 126 126 }); 127 127 ··· 156 156 it('ignores the error when a result is available', async () => { 157 157 fetch.mockResolvedValue({ 158 158 status: 400, 159 - json: jest.fn().mockResolvedValue(response), 159 + text: jest.fn().mockResolvedValue(response), 160 160 }); 161 161 162 162 const data = await pipe( ··· 165 165 toPromise 166 166 ); 167 167 168 - expect(data.data).toEqual(response.data); 168 + expect(data.data).toEqual(JSON.parse(response).data); 169 169 }); 170 170 }); 171 171
+31 -31
exchanges/persisted-fetch/src/persistedFetchExchange.test.ts
··· 28 28 }); 29 29 30 30 it('accepts successful persisted query responses', async () => { 31 - const expected = { 31 + const expected = JSON.stringify({ 32 32 data: { 33 33 test: true, 34 34 }, 35 - }; 35 + }); 36 36 37 37 fetch.mockResolvedValueOnce({ 38 - json: () => Promise.resolve(expected), 38 + text: () => Promise.resolve(expected), 39 39 }); 40 40 41 41 const actual = await pipe( ··· 50 50 }); 51 51 52 52 it('supports cache-miss persisted query errors', async () => { 53 - const expectedMiss = { 53 + const expectedMiss = JSON.stringify({ 54 54 errors: [{ message: 'PersistedQueryNotFound' }], 55 - }; 55 + }); 56 56 57 - const expectedRetry = { 57 + const expectedRetry = JSON.stringify({ 58 58 data: { 59 59 test: true, 60 60 }, 61 - }; 61 + }); 62 62 63 63 fetch 64 64 .mockResolvedValueOnce({ 65 - json: () => Promise.resolve(expectedMiss), 65 + text: () => Promise.resolve(expectedMiss), 66 66 }) 67 67 .mockResolvedValueOnce({ 68 - json: () => Promise.resolve(expectedRetry), 68 + text: () => Promise.resolve(expectedRetry), 69 69 }); 70 70 71 71 const actual = await pipe( ··· 81 81 }); 82 82 83 83 it('supports GET exclusively for persisted queries', async () => { 84 - const expectedMiss = { 84 + const expectedMiss = JSON.stringify({ 85 85 errors: [{ message: 'PersistedQueryNotFound' }], 86 - }; 86 + }); 87 87 88 - const expectedRetry = { 88 + const expectedRetry = JSON.stringify({ 89 89 data: { 90 90 test: true, 91 91 }, 92 - }; 92 + }); 93 93 94 94 fetch 95 95 .mockResolvedValueOnce({ 96 - json: () => Promise.resolve(expectedMiss), 96 + text: () => Promise.resolve(expectedMiss), 97 97 }) 98 98 .mockResolvedValueOnce({ 99 - json: () => Promise.resolve(expectedRetry), 99 + text: () => Promise.resolve(expectedRetry), 100 100 }); 101 101 102 102 const actual = await pipe( ··· 114 114 }); 115 115 116 116 it('supports unsupported persisted query errors', async () => { 117 - const expectedMiss = { 117 + const expectedMiss = JSON.stringify({ 118 118 errors: [{ message: 'PersistedQueryNotSupported' }], 119 - }; 119 + }); 120 120 121 - const expectedRetry = { 121 + const expectedRetry = JSON.stringify({ 122 122 data: { 123 123 test: true, 124 124 }, 125 - }; 125 + }); 126 126 127 127 fetch 128 128 .mockResolvedValueOnce({ 129 - json: () => Promise.resolve(expectedMiss), 129 + text: () => Promise.resolve(expectedMiss), 130 130 }) 131 131 .mockResolvedValueOnce({ 132 - json: () => Promise.resolve(expectedRetry), 132 + text: () => Promise.resolve(expectedRetry), 133 133 }) 134 134 .mockResolvedValueOnce({ 135 - json: () => Promise.resolve(expectedRetry), 135 + text: () => Promise.resolve(expectedRetry), 136 136 }); 137 137 138 138 const actual = await pipe( ··· 148 148 }); 149 149 150 150 it('correctly generates an SHA256 hash', async () => { 151 - const expected = { 151 + const expected = JSON.stringify({ 152 152 data: { 153 153 test: true, 154 154 }, 155 - }; 155 + }); 156 156 157 157 fetch.mockResolvedValue({ 158 - json: () => Promise.resolve(expected), 158 + text: () => Promise.resolve(expected), 159 159 }); 160 160 161 161 const queryHash = await hash(print(queryOperation.query)); ··· 185 185 }); 186 186 187 187 it('supports a custom hash function', async () => { 188 - const expected = { 188 + const expected = JSON.stringify({ 189 189 data: { 190 190 test: true, 191 191 }, 192 - }; 192 + }); 193 193 194 194 fetch.mockResolvedValueOnce({ 195 - json: () => Promise.resolve(expected), 195 + text: () => Promise.resolve(expected), 196 196 }); 197 197 198 198 const hashFn = jest.fn((_input: string, _doc: DocumentNode) => { ··· 228 228 }); 229 229 230 230 it('falls back to a non-persisted query if the hash is falsy', async () => { 231 - const expected = { 231 + const expected = JSON.stringify({ 232 232 data: { 233 233 test: true, 234 234 }, 235 - }; 235 + }); 236 236 237 237 fetch.mockResolvedValueOnce({ 238 - json: () => Promise.resolve(expected), 238 + text: () => Promise.resolve(expected), 239 239 }); 240 240 241 241 const hashFn = jest.fn(() => Promise.resolve(''));
+2 -2
packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
··· 3 3 exports[`on error returns error data 1`] = ` 4 4 Object { 5 5 "data": undefined, 6 - "error": [CombinedError: [Network] ], 6 + "error": [CombinedError: [Network] No Content], 7 7 "extensions": undefined, 8 8 "operation": Object { 9 9 "context": Object { ··· 142 142 exports[`on error returns error data with status 400 and manual redirect mode 1`] = ` 143 143 Object { 144 144 "data": undefined, 145 - "error": [CombinedError: [Network] ], 145 + "error": [CombinedError: [Network] No Content], 146 146 "extensions": undefined, 147 147 "operation": Object { 148 148 "context": Object {
+6 -6
packages/core/src/exchanges/fetch.test.ts
··· 28 28 (global as any).AbortController = undefined; 29 29 }); 30 30 31 - const response = { 31 + const response = JSON.stringify({ 32 32 status: 200, 33 33 data: { 34 34 data: { 35 35 user: 1200, 36 36 }, 37 37 }, 38 - }; 38 + }); 39 39 40 40 const exchangeArgs = { 41 41 dispatchDebug: jest.fn(), ··· 51 51 beforeEach(() => { 52 52 fetch.mockResolvedValue({ 53 53 status: 200, 54 - json: jest.fn().mockResolvedValue(response), 54 + text: jest.fn().mockResolvedValue(response), 55 55 }); 56 56 }); 57 57 ··· 80 80 beforeEach(() => { 81 81 fetch.mockResolvedValue({ 82 82 status: 400, 83 - json: jest.fn().mockResolvedValue({}), 83 + text: jest.fn().mockResolvedValue(JSON.stringify({})), 84 84 }); 85 85 }); 86 86 ··· 115 115 it('ignores the error when a result is available', async () => { 116 116 fetch.mockResolvedValue({ 117 117 status: 400, 118 - json: jest.fn().mockResolvedValue(response), 118 + text: jest.fn().mockResolvedValue(response), 119 119 }); 120 120 121 121 const data = await pipe( ··· 124 124 toPromise 125 125 ); 126 126 127 - expect(data.data).toEqual(response.data); 127 + expect(data.data).toEqual(JSON.parse(response).data); 128 128 }); 129 129 }); 130 130
+3 -3
packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
··· 3 3 exports[`on error ignores the error when a result is available 1`] = ` 4 4 Object { 5 5 "data": undefined, 6 - "error": [CombinedError: [Network] ], 6 + "error": [CombinedError: [Network] Forbidden], 7 7 "extensions": undefined, 8 8 "operation": Object { 9 9 "context": Object { ··· 142 142 exports[`on error returns error data 1`] = ` 143 143 Object { 144 144 "data": undefined, 145 - "error": [CombinedError: [Network] ], 145 + "error": [CombinedError: [Network] Forbidden], 146 146 "extensions": undefined, 147 147 "operation": Object { 148 148 "context": Object { ··· 281 281 exports[`on error returns error data with status 400 and manual redirect mode 1`] = ` 282 282 Object { 283 283 "data": undefined, 284 - "error": [CombinedError: [Network] ], 284 + "error": [CombinedError: [Network] Forbidden], 285 285 "extensions": undefined, 286 286 "operation": Object { 287 287 "context": Object {
+28 -5
packages/core/src/internal/fetchSource.test.ts
··· 28 28 (global as any).AbortController = undefined; 29 29 }); 30 30 31 - const response = { 31 + const response = JSON.stringify({ 32 32 status: 200, 33 33 data: { 34 34 data: { 35 35 user: 1200, 36 36 }, 37 37 }, 38 - }; 38 + }); 39 39 40 40 describe('on success', () => { 41 41 beforeEach(() => { 42 42 fetch.mockResolvedValue({ 43 43 status: 200, 44 - json: jest.fn().mockResolvedValue(response), 44 + text: jest.fn().mockResolvedValue(response), 45 45 }); 46 46 }); 47 47 ··· 63 63 const fetchOptions = {}; 64 64 const fetcher = jest.fn().mockResolvedValue({ 65 65 status: 200, 66 - json: jest.fn().mockResolvedValue(response), 66 + text: jest.fn().mockResolvedValue(response), 67 67 }); 68 68 69 69 const data = await pipe( ··· 91 91 beforeEach(() => { 92 92 fetch.mockResolvedValue({ 93 93 status: 400, 94 - json: jest.fn().mockResolvedValue({}), 94 + statusText: 'Forbidden', 95 + text: jest.fn().mockResolvedValue('{}'), 95 96 }); 96 97 }); 97 98 ··· 123 124 ); 124 125 125 126 expect(data).toMatchSnapshot(); 127 + }); 128 + }); 129 + 130 + describe('on unexpected plain text responses', () => { 131 + beforeEach(() => { 132 + fetch.mockResolvedValue({ 133 + status: 200, 134 + headers: new Map([['Content-Type', 'text/plain']]), 135 + text: jest.fn().mockResolvedValue('Some Error Message'), 136 + }); 137 + }); 138 + 139 + it('returns error data', async () => { 140 + const fetchOptions = {}; 141 + const result = await pipe( 142 + makeFetchSource(queryOperation, 'https://test.com/graphql', fetchOptions), 143 + toPromise 144 + ); 145 + 146 + expect(result.error).toMatchObject({ 147 + message: '[Network] Some Error Message', 148 + }); 126 149 }); 127 150 }); 128 151
+12 -6
packages/core/src/internal/fetchSource.ts
··· 43 43 // NOTE: Guarding against fetch polyfills here 44 44 const contentType = 45 45 (response.headers && response.headers.get('Content-Type')) || ''; 46 - if (!/multipart\/mixed/i.test(contentType)) { 47 - return response.json().then(payload => { 48 - const result = makeResult(operation, payload, response); 49 - hasResults = true; 50 - onResult(result); 46 + if (/text\//i.test(contentType)) { 47 + return response.text().then(text => { 48 + onResult(makeErrorResult(operation, new Error(text), response)); 49 + }); 50 + } else if (!/multipart\/mixed/i.test(contentType)) { 51 + return response.text().then(payload => { 52 + onResult(makeResult(operation, JSON.parse(payload), response)); 51 53 }); 52 54 } 53 55 ··· 160 162 161 163 const result = makeErrorResult( 162 164 operation, 163 - statusNotOk ? new Error(response.statusText) : error, 165 + statusNotOk 166 + ? response.statusText 167 + ? new Error(response.statusText) 168 + : error 169 + : error, 164 170 response 165 171 ); 166 172