Skip to content

Commit 796a5bc

Browse files
committed
fix(http): keep the response text when a JSON response fails to parse with fetch
When a 2xx response with `responseType: 'json'` could not be parsed, FetchBackend reported the bare `SyntaxError` as `HttpErrorResponse.error`, so the response body was lost. HttpXhrBackend reports `{error, text}` in this case, which lets apps show or log the body (for example an HTML maintenance page returned with status 200). Report the same `{error, text}` shape from FetchBackend, keeping the original text including any XSSI prefix, as the XHR backend does.
1 parent e699fd7 commit 796a5bc

2 files changed

Lines changed: 25 additions & 2 deletions

File tree

‎packages/common/http/src/fetch.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
HttpEvent,
2929
HttpEventType,
3030
HttpHeaderResponse,
31+
HttpJsonParseError,
3132
HttpResponse,
3233
} from './response';
3334

@@ -342,8 +343,9 @@ export class FetchBackend implements HttpBackend {
342343
): string | ArrayBuffer | Blob | object | null {
343344
switch (request.responseType) {
344345
case 'json':
346+
const originalText = new TextDecoder().decode(binContent);
345347
// stripping the XSSI when present
346-
const text = new TextDecoder().decode(binContent).replace(XSSI_PREFIX, '');
348+
const text = originalText.replace(XSSI_PREFIX, '');
347349
if (text === '') {
348350
return null;
349351
}
@@ -357,7 +359,8 @@ export class FetchBackend implements HttpBackend {
357359
if (status < 200 || status >= 300) {
358360
return text;
359361
}
360-
throw e;
362+
// Like the XHR backend, keep the body that failed to parse.
363+
throw {error: e, text: originalText} as HttpJsonParseError;
361364
}
362365
case 'text':
363366
return getTextDecoder(contentType).decode(binContent);

‎packages/common/http/test/fetch_spec.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,26 @@ describe('FetchBackend', () => {
297297
expect(res.error).toBe('simple text error');
298298
});
299299

300+
it('keeps the response text when a json success response fails to parse', async () => {
301+
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
302+
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', '<html>Service unavailable</html>');
303+
const events = await promise;
304+
expect(events.length).toBe(2);
305+
const res = events[1] as any as HttpErrorResponse;
306+
expect(res.status).toBe(HttpStatusCode.Ok);
307+
expect(res.error.error).toBeInstanceOf(SyntaxError);
308+
expect(res.error.text).toBe('<html>Service unavailable</html>');
309+
});
310+
311+
it('keeps the XSSI prefix in the text when a json success response fails to parse', async () => {
312+
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
313+
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', XSSI_PREFIX + 'not json');
314+
const events = await promise;
315+
const res = events[1] as any as HttpErrorResponse;
316+
expect(res.error.error).toBeInstanceOf(SyntaxError);
317+
expect(res.error.text).toBe(XSSI_PREFIX + 'not json');
318+
});
319+
300320
it('handles a json string response', async () => {
301321
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
302322
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', JSON.stringify('this is a string'));

0 commit comments

Comments
 (0)