Skip to content

Commit 15670d8

Browse files
sbarfurththePunderWoman
authored andcommitted
fix(http): propagate plain errors when parsing fails (#62765)
The fetch backend now propagates the plain body when parsing the body fails. This replicates the behavior of the XHR backend introduced in #19773. The current state completely obfuscates errors of the "wrong" response type. However, it's not uncommon for successful requests to return one type and errors to return another type. Propagating the plain error allows downstream error consumers to reason about the error body and decide how to parse it depending on application needs. PR Close #62765
1 parent a83ef0e commit 15670d8

File tree

2 files changed

+39
-2
lines changed

2 files changed

+39
-2
lines changed

packages/common/http/src/fetch.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ export class FetchBackend implements HttpBackend {
244244
const chunksAll = this.concatChunks(chunks, receivedLength);
245245
try {
246246
const contentType = response.headers.get(CONTENT_TYPE_HEADER) ?? '';
247-
body = this.parseBody(request, chunksAll, contentType);
247+
body = this.parseBody(request, chunksAll, contentType, status);
248248
} catch (error) {
249249
// Body loading or parsing failed
250250
observer.error(
@@ -302,12 +302,27 @@ export class FetchBackend implements HttpBackend {
302302
request: HttpRequest<any>,
303303
binContent: Uint8Array,
304304
contentType: string,
305+
status: number,
305306
): string | ArrayBuffer | Blob | object | null {
306307
switch (request.responseType) {
307308
case 'json':
308309
// stripping the XSSI when present
309310
const text = new TextDecoder().decode(binContent).replace(XSSI_PREFIX, '');
310-
return text === '' ? null : (JSON.parse(text) as object);
311+
if (text === '') {
312+
return null;
313+
}
314+
try {
315+
return JSON.parse(text) as object;
316+
} catch (e: unknown) {
317+
// Allow handling non-JSON errors (!) as plain text, same as the XHR
318+
// backend. Without this special sauce, any non-JSON error would be
319+
// completely inaccessible downstream as the `HttpErrorResponse.error`
320+
// would be set to the `SyntaxError` from then failing `JSON.parse`.
321+
if (status < 200 || status >= 300) {
322+
return text;
323+
}
324+
throw e;
325+
}
311326
case 'text':
312327
return new TextDecoder().decode(binContent);
313328
case 'blob':

packages/common/http/test/fetch_spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,15 @@ describe('FetchBackend', async () => {
213213
expect(res.error.data).toBe('some data');
214214
});
215215

216+
it('handles a text error response when a json success response was expected', async () => {
217+
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
218+
fetchMock.mockFlush(HttpStatusCode.InternalServerError, 'Error', 'simple text error');
219+
const events = await promise;
220+
expect(events.length).toBe(2);
221+
const res = events[1] as any as HttpErrorResponse;
222+
expect(res.error).toBe('simple text error');
223+
});
224+
216225
it('handles a json error response with XSSI prefix', async () => {
217226
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
218227
fetchMock.mockFlush(
@@ -226,6 +235,19 @@ describe('FetchBackend', async () => {
226235
expect(res.error.data).toBe('some data');
227236
});
228237

238+
it('handles a text error response with XSSI prefix when a json success response was expected', async () => {
239+
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
240+
fetchMock.mockFlush(
241+
HttpStatusCode.InternalServerError,
242+
'Error',
243+
XSSI_PREFIX + 'simple text error',
244+
);
245+
const events = await promise;
246+
expect(events.length).toBe(2);
247+
const res = events[1] as any as HttpErrorResponse;
248+
expect(res.error).toBe('simple text error');
249+
});
250+
229251
it('handles a json string response', async () => {
230252
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
231253
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', JSON.stringify('this is a string'));

0 commit comments

Comments
 (0)