Skip to content

Commit 1d6e65d

Browse files
author
Camille Croci
committed
feat: catch the expections from response.text()
1 parent a7b9646 commit 1d6e65d

File tree

3 files changed

+94
-3
lines changed

3 files changed

+94
-3
lines changed

packages/fetch-error-handler/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Properly handle fetch errors and avoid a lot of boilerplate in your app. This mo
1414
* [Abort and timeout errors](#abort-and-timeout-errors)
1515
* [Socket errors](#socket-errors)
1616
* [Invalid JSON errors](#invalid-json-errors)
17+
* [Invalid body errors](#invalid-body-errors)
1718
* [Unknown errors](#unknown-errors)
1819
* [Creating your own handler](#creating-your-own-handler)
1920
* [`createFetchErrorHandler` configuration options](#createfetcherrorhandler-configuration-options)
@@ -158,6 +159,17 @@ error.data.responseBody // The body of the initial response
158159
> [!IMPORTANT]<br />
159160
> If the body is too long, we are truncating it because Splunk has a limit of characters to log.
160161
162+
#### Invalid body errors
163+
164+
When decrypting the body of the reponse, it is possible that the .text() method throw an [exception](https://developer.mozilla.org/en-US/docs/Web/API/Response/text#exceptions). In that case, we will throw an [`OperationalError`](https://github.com/Financial-Times/dotcom-reliability-kit/tree/main/packages/errors#operationalerror). This error will have the following properties to help you debug:
165+
166+
```js
167+
error.statusCode // 502
168+
error.code // FETCH_BODY_ABORT_ERROR or FETCH_BODY_TYPE_ERROR depending on the cause
169+
error.message // A more human readable explaination of the exception
170+
error.cause // The cause of the error parsing the body
171+
```
172+
161173
#### Unknown errors
162174

163175
If the URL you fetched responds with an `ok` property of `false` and a status code outside of the `400–599` range, then it's unclear what's happened but we reject with an error anyway to make sure we're able to debug. We output an [`HTTPError`](https://github.com/Financial-Times/dotcom-reliability-kit/tree/main/packages/errors#httperror):

packages/fetch-error-handler/lib/create-handler.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,38 @@ function createFetchErrorHandler(options = {}) {
177177
// And we want the consuming apps to still be able to read it if necessary
178178
const clonedResponse = response.clone();
179179

180-
const contentType = clonedResponse.headers?.get('content-type');
181-
const rawResponseBody = await clonedResponse.text();
180+
let rawResponseBody;
181+
try {
182+
rawResponseBody = await clonedResponse.text();
183+
} catch (/** @type {any} */ error) {
184+
if (error?.name === 'AbortError') {
185+
throw new OperationalError({
186+
code: 'FETCH_BODY_ABORT_ERROR',
187+
message:
188+
'The request was cancelled or the connection was closed before the full body could be read.',
189+
relatesToSystems,
190+
cause: error
191+
});
192+
}
193+
194+
if (error?.name === 'TypeError') {
195+
throw new OperationalError({
196+
code: 'FETCH_BODY_TYPE_ERROR',
197+
message:
198+
'The body might be distubed or locked, or the content encoding was invalid',
199+
relatesToSystems,
200+
cause: error
201+
});
202+
}
203+
204+
// We don't know what to do with this error so
205+
// we throw it as-is
206+
throw error;
207+
}
182208

209+
const contentType = clonedResponse.headers?.get('content-type');
183210
let bodyIsInvalidJson = false;
184-
if (contentType?.includes('application/json')) {
211+
if (contentType?.includes('application/json') && rawResponseBody) {
185212
try {
186213
// Attempt to parse the response body as JSON...
187214
responseBody = JSON.parse(rawResponseBody);

packages/fetch-error-handler/test/unit/lib/create-handler.spec.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,58 @@ describe('@dotcom-reliability-kit/fetch-error-handler/lib/create-handler', () =>
292292
}
293293
});
294294
});
295+
296+
describe('when `response.text()` throws an AbortError exception', () => {
297+
it('rejects with an augmented error', async () => {
298+
const mockError = Object.assign(new Error('mock abort error'), {
299+
name: 'AbortError'
300+
});
301+
302+
const mockResponse = {
303+
ok: true,
304+
status: 200,
305+
url: 'https://mock.com/example',
306+
headers: {
307+
get: jest.fn(() => 'application/json')
308+
},
309+
clone: jest.fn(() => mockResponse),
310+
text: jest.fn().mockRejectedValue(mockError)
311+
};
312+
expect.assertions(4);
313+
try {
314+
await fetchErrorHandler(mockResponse);
315+
} catch (error) {
316+
expect(error).toBeInstanceOf(Error);
317+
expect(error.name).toStrictEqual('OperationalError');
318+
expect(error.code).toStrictEqual('FETCH_BODY_ABORT_ERROR');
319+
expect(error.cause).toStrictEqual(mockError);
320+
}
321+
});
322+
});
323+
324+
describe('when `response.text()` throws an TypeError exception', () => {
325+
it('rejects with an augmented error', async () => {
326+
const mockResponse = {
327+
ok: true,
328+
status: 200,
329+
url: 'https://mock.com/example',
330+
headers: {
331+
get: jest.fn(() => 'application/json')
332+
},
333+
clone: jest.fn(() => mockResponse),
334+
text: jest.fn().mockRejectedValue(new TypeError('mock body lock'))
335+
};
336+
expect.assertions(4);
337+
try {
338+
await fetchErrorHandler(mockResponse);
339+
} catch (error) {
340+
expect(error).toBeInstanceOf(Error);
341+
expect(error.name).toStrictEqual('OperationalError');
342+
expect(error.code).toStrictEqual('FETCH_BODY_TYPE_ERROR');
343+
expect(error.cause.message).toStrictEqual('mock body lock');
344+
}
345+
});
346+
});
295347
});
296348

297349
describe('fetchErrorHandler(responsePromise)', () => {

0 commit comments

Comments
 (0)