diff --git a/.changeset/quiet-insects-shake.md b/.changeset/quiet-insects-shake.md new file mode 100644 index 0000000000..0c2c4c29b6 --- /dev/null +++ b/.changeset/quiet-insects-shake.md @@ -0,0 +1,5 @@ +--- +'@lion/ajax': minor +--- + +Parses response body automatically for fetchJson failed responses. Add test for reading out the response body in cases of a failed response in regular fetch call. diff --git a/package-lock.json b/package-lock.json index 199ea2f40c..9dfcb0addc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22384,7 +22384,7 @@ }, "packages/ajax": { "name": "@lion/ajax", - "version": "1.1.3", + "version": "1.1.4", "license": "MIT" }, "packages/singleton-manager": { diff --git a/packages/ajax/src/Ajax.js b/packages/ajax/src/Ajax.js index 4b6e5e1a1d..933c6794dd 100644 --- a/packages/ajax/src/Ajax.js +++ b/packages/ajax/src/Ajax.js @@ -124,9 +124,10 @@ export class Ajax { * * @param {RequestInfo} info * @param {RequestInit & Partial} [init] + * @param {Boolean} [parseErrorResponse] * @returns {Promise} */ - async fetch(info, init) { + async fetch(info, init, parseErrorResponse = false) { const request = /** @type {CacheRequest} */ (new Request(info, { ...init })); request.cacheOptions = init?.cacheOptions; request.params = init?.params; @@ -137,7 +138,11 @@ export class Ajax { const response = /** @type {CacheResponse} */ (interceptedRequestOrResponse); response.request = request; if (isFailedResponse(interceptedRequestOrResponse)) { - throw new AjaxFetchError(request, response); + throw new AjaxFetchError( + request, + response, + parseErrorResponse ? await this.__attemptParseFailedResponseBody(response) : undefined, + ); } // prevent network request, return cached response return response; @@ -149,7 +154,11 @@ export class Ajax { const interceptedResponse = await this.__interceptResponse(response); if (isFailedResponse(interceptedResponse)) { - throw new AjaxFetchError(request, interceptedResponse); + throw new AjaxFetchError( + request, + response, + parseErrorResponse ? await this.__attemptParseFailedResponseBody(response) : undefined, + ); } return interceptedResponse; } @@ -163,8 +172,7 @@ export class Ajax { * * @param {RequestInfo} info * @param {LionRequestInit} [init] - * @template T - * @returns {Promise<{ response: Response, body: T }>} + * @returns {Promise<{ response: Response, body: string|Object }>} */ async fetchJson(info, init) { const lionInit = { @@ -183,7 +191,18 @@ export class Ajax { // typecast LionRequestInit back to RequestInit const jsonInit = /** @type {RequestInit} */ (lionInit); - const response = await this.fetch(info, jsonInit); + const response = await this.fetch(info, jsonInit, true); + + const body = await this.__parseBody(response); + + return { response, body }; + } + + /** + * @param {Response} response + * @returns {Promise} + */ + async __parseBody(response) { let responseText = await response.text(); const { jsonPrefix } = this.__config; @@ -191,7 +210,6 @@ export class Ajax { responseText = responseText.substring(jsonPrefix.length); } - /** @type {any} */ let body = responseText; if ( @@ -207,8 +225,21 @@ export class Ajax { } else { body = responseText; } + return body; + } - return { response, body }; + /** + * @param {Response} response + * @returns {Promise} + */ + async __attemptParseFailedResponseBody(response) { + let body; + try { + body = await this.__parseBody(response); + } catch (e) { + // no need to throw/log, failed responses often don't have a body + } + return body; } /** diff --git a/packages/ajax/src/AjaxFetchError.js b/packages/ajax/src/AjaxFetchError.js index 258bfe6729..7fbd66e949 100644 --- a/packages/ajax/src/AjaxFetchError.js +++ b/packages/ajax/src/AjaxFetchError.js @@ -2,10 +2,12 @@ export class AjaxFetchError extends Error { /** * @param {Request} request * @param {Response} response + * @param {string|Object} [body] */ - constructor(request, response) { + constructor(request, response, body) { super(`Fetch request to ${request.url} failed.`); this.request = request; this.response = response; + this.body = body; } } diff --git a/packages/ajax/test/Ajax.test.js b/packages/ajax/test/Ajax.test.js index 1a4b88c45d..d990cfa164 100644 --- a/packages/ajax/test/Ajax.test.js +++ b/packages/ajax/test/Ajax.test.js @@ -139,6 +139,25 @@ describe('Ajax', () => { } expect(thrown).to.be.true; }); + + it('throws on 4xx responses, will allow parsing response manually', async () => { + ajax.addRequestInterceptor(async () => new Response('my response', { status: 400 })); + + let thrown = false; + try { + await ajax.fetch('/foo'); + } catch (e) { + // https://github.com/microsoft/TypeScript/issues/20024 open issue, can't type catch clause in param + const _e = /** @type {AjaxFetchError} */ (e); + expect(_e).to.be.an.instanceOf(AjaxFetchError); + expect(_e.request).to.be.an.instanceOf(Request); + expect(_e.response).to.be.an.instanceOf(Response); + const body = await _e.response.text(); + expect(body).to.equal('my response'); + thrown = true; + } + expect(thrown).to.be.true; + }); }); describe('fetchJson', () => { @@ -183,6 +202,24 @@ describe('Ajax', () => { expect(response.body).to.eql({ a: 1, b: 2 }); }); + it('throws on 4xx responses, but still attempts parsing response body when using fetchJson', async () => { + ajax.addRequestInterceptor(async () => new Response('my response', { status: 400 })); + + let thrown = false; + try { + await ajax.fetchJson('/foo'); + } catch (e) { + // https://github.com/microsoft/TypeScript/issues/20024 open issue, can't type catch clause in param + const _e = /** @type {AjaxFetchError} */ (e); + expect(_e).to.be.an.instanceOf(AjaxFetchError); + expect(_e.request).to.be.an.instanceOf(Request); + expect(_e.response).to.be.an.instanceOf(Response); + expect(_e.body).to.equal('my response'); + thrown = true; + } + expect(thrown).to.be.true; + }); + describe('given a request body', () => { it('encodes the request body as json', async () => { await ajax.fetchJson('/foo', { method: 'POST', body: { a: 1, b: 2 } });