feat: parsing body for failed ajax responses (#2039)
This commit is contained in:
parent
5eafa1ffc1
commit
7a875ef1b4
5 changed files with 85 additions and 10 deletions
5
.changeset/quiet-insects-shake.md
Normal file
5
.changeset/quiet-insects-shake.md
Normal file
|
|
@ -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.
|
||||||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -22384,7 +22384,7 @@
|
||||||
},
|
},
|
||||||
"packages/ajax": {
|
"packages/ajax": {
|
||||||
"name": "@lion/ajax",
|
"name": "@lion/ajax",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"packages/singleton-manager": {
|
"packages/singleton-manager": {
|
||||||
|
|
|
||||||
|
|
@ -124,9 +124,10 @@ export class Ajax {
|
||||||
*
|
*
|
||||||
* @param {RequestInfo} info
|
* @param {RequestInfo} info
|
||||||
* @param {RequestInit & Partial<CacheRequestExtension>} [init]
|
* @param {RequestInit & Partial<CacheRequestExtension>} [init]
|
||||||
|
* @param {Boolean} [parseErrorResponse]
|
||||||
* @returns {Promise<Response>}
|
* @returns {Promise<Response>}
|
||||||
*/
|
*/
|
||||||
async fetch(info, init) {
|
async fetch(info, init, parseErrorResponse = false) {
|
||||||
const request = /** @type {CacheRequest} */ (new Request(info, { ...init }));
|
const request = /** @type {CacheRequest} */ (new Request(info, { ...init }));
|
||||||
request.cacheOptions = init?.cacheOptions;
|
request.cacheOptions = init?.cacheOptions;
|
||||||
request.params = init?.params;
|
request.params = init?.params;
|
||||||
|
|
@ -137,7 +138,11 @@ export class Ajax {
|
||||||
const response = /** @type {CacheResponse} */ (interceptedRequestOrResponse);
|
const response = /** @type {CacheResponse} */ (interceptedRequestOrResponse);
|
||||||
response.request = request;
|
response.request = request;
|
||||||
if (isFailedResponse(interceptedRequestOrResponse)) {
|
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
|
// prevent network request, return cached response
|
||||||
return response;
|
return response;
|
||||||
|
|
@ -149,7 +154,11 @@ export class Ajax {
|
||||||
const interceptedResponse = await this.__interceptResponse(response);
|
const interceptedResponse = await this.__interceptResponse(response);
|
||||||
|
|
||||||
if (isFailedResponse(interceptedResponse)) {
|
if (isFailedResponse(interceptedResponse)) {
|
||||||
throw new AjaxFetchError(request, interceptedResponse);
|
throw new AjaxFetchError(
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
parseErrorResponse ? await this.__attemptParseFailedResponseBody(response) : undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return interceptedResponse;
|
return interceptedResponse;
|
||||||
}
|
}
|
||||||
|
|
@ -163,8 +172,7 @@ export class Ajax {
|
||||||
*
|
*
|
||||||
* @param {RequestInfo} info
|
* @param {RequestInfo} info
|
||||||
* @param {LionRequestInit} [init]
|
* @param {LionRequestInit} [init]
|
||||||
* @template T
|
* @returns {Promise<{ response: Response, body: string|Object }>}
|
||||||
* @returns {Promise<{ response: Response, body: T }>}
|
|
||||||
*/
|
*/
|
||||||
async fetchJson(info, init) {
|
async fetchJson(info, init) {
|
||||||
const lionInit = {
|
const lionInit = {
|
||||||
|
|
@ -183,7 +191,18 @@ export class Ajax {
|
||||||
|
|
||||||
// typecast LionRequestInit back to RequestInit
|
// typecast LionRequestInit back to RequestInit
|
||||||
const jsonInit = /** @type {RequestInit} */ (lionInit);
|
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<string|Object>}
|
||||||
|
*/
|
||||||
|
async __parseBody(response) {
|
||||||
let responseText = await response.text();
|
let responseText = await response.text();
|
||||||
|
|
||||||
const { jsonPrefix } = this.__config;
|
const { jsonPrefix } = this.__config;
|
||||||
|
|
@ -191,7 +210,6 @@ export class Ajax {
|
||||||
responseText = responseText.substring(jsonPrefix.length);
|
responseText = responseText.substring(jsonPrefix.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {any} */
|
|
||||||
let body = responseText;
|
let body = responseText;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -207,8 +225,21 @@ export class Ajax {
|
||||||
} else {
|
} else {
|
||||||
body = responseText;
|
body = responseText;
|
||||||
}
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
return { response, body };
|
/**
|
||||||
|
* @param {Response} response
|
||||||
|
* @returns {Promise<string|Object|undefined>}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ export class AjaxFetchError extends Error {
|
||||||
/**
|
/**
|
||||||
* @param {Request} request
|
* @param {Request} request
|
||||||
* @param {Response} response
|
* @param {Response} response
|
||||||
|
* @param {string|Object} [body]
|
||||||
*/
|
*/
|
||||||
constructor(request, response) {
|
constructor(request, response, body) {
|
||||||
super(`Fetch request to ${request.url} failed.`);
|
super(`Fetch request to ${request.url} failed.`);
|
||||||
this.request = request;
|
this.request = request;
|
||||||
this.response = response;
|
this.response = response;
|
||||||
|
this.body = body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,25 @@ describe('Ajax', () => {
|
||||||
}
|
}
|
||||||
expect(thrown).to.be.true;
|
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', () => {
|
describe('fetchJson', () => {
|
||||||
|
|
@ -183,6 +202,24 @@ describe('Ajax', () => {
|
||||||
expect(response.body).to.eql({ a: 1, b: 2 });
|
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', () => {
|
describe('given a request body', () => {
|
||||||
it('encodes the request body as json', async () => {
|
it('encodes the request body as json', async () => {
|
||||||
await ajax.fetchJson('/foo', { method: 'POST', body: { a: 1, b: 2 } });
|
await ajax.fetchJson('/foo', { method: 'POST', body: { a: 1, b: 2 } });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue