fix(ajax): rename to ajax, async interceptors
This commit is contained in:
parent
c0659a8d5d
commit
4452d06d44
28 changed files with 584 additions and 7202 deletions
5
.changeset/blue-shoes-shave.md
Normal file
5
.changeset/blue-shoes-shave.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@lion/ajax': minor
|
||||
---
|
||||
|
||||
BREAKING CHANGE: We no longer use axios! Our ajax package is now a thin wrapper around Fetch. The API has changed completely. You will need a fetch polyfill for IE11.
|
||||
|
|
@ -1,198 +1,99 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Ajax
|
||||
|
||||
`ajax` is the global manager for handling all ajax requests.
|
||||
It is a promise based system for fetching data, based on [axios](https://github.com/axios/axios)
|
||||
`ajax` is a small wrapper around `fetch` which:
|
||||
|
||||
```js script
|
||||
import { html } from '@lion/core';
|
||||
import { ajax } from './src/ajax.js';
|
||||
import { AjaxClass } from './src/AjaxClass.js';
|
||||
|
||||
export default {
|
||||
title: 'Others/Ajax',
|
||||
};
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- only JS functions, no (unnecessarily expensive) web components
|
||||
- supports GET, POST, PUT, DELETE, REQUEST, PATCH and HEAD methods
|
||||
- can be used with or without XSRF token
|
||||
- Allows globally registering request and response interceptors
|
||||
- Throws on 4xx and 5xx status codes
|
||||
- Prevents network request if a request interceptor returns a response
|
||||
- Supports a JSON request which automatically encodes/decodes body request and response payload as JSON
|
||||
- Adds accept-language header to requests based on application language
|
||||
- Adds XSRF header to request if the cookie is present
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
```sh
|
||||
npm i --save @lion/ajax
|
||||
```
|
||||
|
||||
```js
|
||||
import { ajax, AjaxClass } from '@lion/ajax';
|
||||
```
|
||||
### Relation to fetch
|
||||
|
||||
### Example
|
||||
`ajax` delegates all requests to fetch. `ajax.request` and `ajax.requestJson` have the same function signature as `window.fetch`, you can use any online resource to learn more about fetch. [MDN](http://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) is a great start.
|
||||
|
||||
### Example requests
|
||||
|
||||
#### GET request
|
||||
|
||||
```js
|
||||
import { ajax } from '@lion/ajax';
|
||||
|
||||
ajax.get('data.json').then(response => console.log(response));
|
||||
const response = await ajax.request('/api/users');
|
||||
const users = await response.json();
|
||||
```
|
||||
|
||||
### Performing requests
|
||||
|
||||
Performing a `GET` request:
|
||||
|
||||
```js preview-story
|
||||
export const performingGetRequests = () => html`
|
||||
<button
|
||||
@click=${() => {
|
||||
ajax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Request to Action Logger
|
||||
</button>
|
||||
`;
|
||||
```
|
||||
|
||||
To post data to the server, pass the data as the second argument in the `POST` request:
|
||||
#### POST request
|
||||
|
||||
```js
|
||||
const body = {
|
||||
ant: {
|
||||
type: 'insect',
|
||||
limbs: 6,
|
||||
},
|
||||
};
|
||||
ajax
|
||||
.post('zooApi/animals/addAnimal', body)
|
||||
.then(response => {
|
||||
console.log(`POST successful: ${response.status} ${response.statusText}`);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
import { ajax } from '@lion/ajax';
|
||||
|
||||
const response = await ajax.request('/api/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username: 'steve' }),
|
||||
});
|
||||
const newUser = await response.json();
|
||||
```
|
||||
|
||||
## Configuration
|
||||
### JSON requests
|
||||
|
||||
### JSON prefix
|
||||
We usually deal with JSON requests and responses. With `requestJson` you don't need to specifically stringify the request body or parse the response body:
|
||||
|
||||
The called API might add a JSON prefix to the response in order to prevent hijacking.
|
||||
The prefix renders the string syntactically invalid as a script so that it cannot be hijacked.
|
||||
This prefix should be stripped before parsing the string as JSON.
|
||||
Pass the prefix with the `jsonPrefix` option.
|
||||
#### GET JSON request
|
||||
|
||||
```js
|
||||
const myAjax = new AjaxClass({ jsonPrefix: ")]}'," });
|
||||
myAjax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
import { ajax } from '@lion/ajax';
|
||||
|
||||
const { response, body } = await ajax.requestJson('/api/users');
|
||||
```
|
||||
|
||||
#### POST JSON request
|
||||
|
||||
```js
|
||||
import { ajax } from '@lion/ajax';
|
||||
|
||||
const { response, body } = await ajax.requestJson('/api/users', {
|
||||
method: 'POST',
|
||||
body: { username: 'steve' },
|
||||
});
|
||||
```
|
||||
|
||||
### Additional headers
|
||||
### Error handling
|
||||
|
||||
Add additional headers to the requests with the `headers` option.
|
||||
Different from fetch, `ajax` throws when the server returns a 4xx or 5xx, returning the request and response:
|
||||
|
||||
```js preview-story
|
||||
export const additionalHeaders = () => html`
|
||||
<button
|
||||
@click=${() => {
|
||||
const myAjax = new AjaxClass({ headers: { 'MY-HEADER': 'SOME-HEADER-VALUE' } });
|
||||
myAjax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Request to Action Logger
|
||||
</button>
|
||||
`;
|
||||
```js
|
||||
import { ajax } from '@lion/ajax';
|
||||
|
||||
try {
|
||||
const users = await ajax.requestJson('/api/users');
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
if (error.response.status === 400) {
|
||||
// handle a specific status code, for example 400 bad request
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
// an error happened before receiving a response, ex. an incorrect request or network error
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When executing the request above, check the Network tab in the Browser's dev tools and look for the Request Header on the GET call.
|
||||
## Fetch Polyfill
|
||||
|
||||
### Cancelable Request
|
||||
For IE11 you will need a polyfill for fetch. You should add this on your top level layer, e.g. your application.
|
||||
|
||||
It is possible to make an Ajax request cancelable, and then call `cancel()` to make the request provide a custom error once fired.
|
||||
|
||||
```js preview-story
|
||||
export const cancelableRequests = () => html`
|
||||
<button
|
||||
@click=${() => {
|
||||
const myAjax = new AjaxClass({ cancelable: true });
|
||||
requestAnimationFrame(() => {
|
||||
myAjax.cancel('too slow');
|
||||
});
|
||||
myAjax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Request to Action Logger
|
||||
</button>
|
||||
`;
|
||||
```
|
||||
|
||||
### Cancel concurrent requests
|
||||
|
||||
You can cancel concurrent requests with the `cancelPreviousOnNewRequest` option.
|
||||
|
||||
```js preview-story
|
||||
export const cancelConcurrentRequests = () => html`
|
||||
<button
|
||||
@click=${() => {
|
||||
const myAjax = new AjaxClass({ cancelPreviousOnNewRequest: true });
|
||||
myAjax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error.message);
|
||||
});
|
||||
myAjax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Both Requests to Action Logger
|
||||
</button>
|
||||
`;
|
||||
```
|
||||
|
||||
## Considerations
|
||||
|
||||
Due to a [bug in axios](https://github.com/axios/axios/issues/385) options may leak in to other instances.
|
||||
So please avoid setting global options in axios. Interceptors have no issues.
|
||||
|
||||
## Future plans
|
||||
|
||||
- Eventually we want to remove axios and replace it with [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
||||
- This wrapper exist to prevent this switch from causing breaking changes for our users
|
||||
[This is the polyfill we recommend](https://github.com/github/fetch). It also has a [section for polyfilling AbortController](https://github.com/github/fetch#aborting-requests)
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"animals": {
|
||||
"cow": {
|
||||
"type": "mammal",
|
||||
"limbs": 4
|
||||
},
|
||||
"frog": {
|
||||
"type": "amphibian",
|
||||
"limbs": 4
|
||||
},
|
||||
"snake": {
|
||||
"type": "reptile",
|
||||
"limbs": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
export { ajax, setAjax } from './src/ajax.js';
|
||||
|
||||
export { AjaxClass } from './src/AjaxClass.js';
|
||||
export { AjaxClient } from './src/AjaxClient.js';
|
||||
|
||||
export {
|
||||
cancelInterceptorFactory,
|
||||
cancelPreviousOnNewRequestInterceptorFactory,
|
||||
addAcceptLanguageHeaderInterceptorFactory,
|
||||
acceptLanguageRequestInterceptor,
|
||||
createXSRFRequestInterceptor,
|
||||
} from './src/interceptors.js';
|
||||
|
||||
export { jsonPrefixTransformerFactory } from './src/transformers.js';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@lion/ajax",
|
||||
"version": "0.5.15",
|
||||
"description": "Thin wrapper around axios to allow for custom interceptors",
|
||||
"description": "Thin wrapper around fetch.",
|
||||
"license": "MIT",
|
||||
"author": "ing-bank",
|
||||
"homepage": "https://github.com/ing-bank/lion/",
|
||||
|
|
@ -29,14 +29,13 @@
|
|||
"prepublishOnly": "../../scripts/npm-prepublish.js",
|
||||
"test": "cd ../../ && npm run test:browser -- --group ajax"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@bundled-es-modules/axios": "0.18.1",
|
||||
"@lion/core": "0.13.8",
|
||||
"singleton-manager": "1.2.1"
|
||||
"@lion/localize": "0.15.5"
|
||||
},
|
||||
"keywords": [
|
||||
"ajax",
|
||||
"fetch",
|
||||
"http",
|
||||
"lion",
|
||||
"web-components"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,254 +0,0 @@
|
|||
// @ts-ignore no types for bundled-es-modules/axios
|
||||
import { axios } from '@bundled-es-modules/axios';
|
||||
import {
|
||||
cancelInterceptorFactory,
|
||||
cancelPreviousOnNewRequestInterceptorFactory,
|
||||
addAcceptLanguageHeaderInterceptorFactory,
|
||||
} from './interceptors.js';
|
||||
import { jsonPrefixTransformerFactory } from './transformers.js';
|
||||
|
||||
/**
|
||||
* @typedef {(config: {[key:string]: ?}) => { transformRequest: (data: string, headers: { [key: string]: any; }) => any;}} RequestInterceptor
|
||||
* @typedef {(config: {[key:string]: ?}) => Response} ResponseInterceptor
|
||||
*
|
||||
* @typedef {Object} AjaxConfig
|
||||
* @property {string} [jsonPrefix] prefixing the JSON string in this manner is used to help
|
||||
* prevent JSON Hijacking. The prefix renders the string syntactically invalid as a script so
|
||||
* that it cannot be hijacked. This prefix should be stripped before parsing the string as JSON.
|
||||
* @property {string} [lang] language
|
||||
* @property {boolean} [languageHeader] the Accept-Language request HTTP header advertises
|
||||
* which languages the client is able to understand, and which locale variant is preferred.
|
||||
* @property {boolean} [cancelable] if request can be canceled
|
||||
* @property {boolean} [cancelPreviousOnNewRequest] prevents concurrent requests
|
||||
*/
|
||||
|
||||
/**
|
||||
* `AjaxClass` creates the singleton instance {@link:ajax}. It is a promise based system for
|
||||
* fetching data, based on [axios](https://github.com/axios/axios).
|
||||
*/
|
||||
export class AjaxClass {
|
||||
/**
|
||||
* @property {Object} proxy the axios instance that is bound to the AjaxClass instance
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {AjaxConfig} [config] configuration for the AjaxClass instance
|
||||
*/
|
||||
constructor(config) {
|
||||
this.__config = {
|
||||
lang: document.documentElement.getAttribute('lang'),
|
||||
languageHeader: true,
|
||||
cancelable: false,
|
||||
cancelPreviousOnNewRequest: false,
|
||||
...config,
|
||||
};
|
||||
this.proxy = axios.create(this.__config);
|
||||
this.__setupInterceptors();
|
||||
|
||||
/** @type {Array.<RequestInterceptor>} */
|
||||
this.requestInterceptors = [];
|
||||
/** @type {Array.<RequestInterceptor>} */
|
||||
this.requestErrorInterceptors = [];
|
||||
/** @type {Array.<RequestInterceptor>} */
|
||||
this.responseErrorInterceptors = [];
|
||||
/** @type {Array.<ResponseInterceptor>} */
|
||||
this.responseInterceptors = [];
|
||||
|
||||
/** @type {Array.<(data: string, headers?: {[key:string]: ?}) => string>} */
|
||||
this.requestDataTransformers = [];
|
||||
/** @type {Array.<(data: string, headers?: {[key:string]: ?}) => string>} */
|
||||
this.requestDataErrorTransformers = [];
|
||||
/** @type {Array.<(data: string, headers?: {[key:string]: ?}) => string>} */
|
||||
this.responseDataErrorTransformers = [];
|
||||
/** @type {Array.<(data: string, headers?: {[key:string]: ?}) => string>} */
|
||||
this.responseDataTransformers = [];
|
||||
|
||||
this.__isInterceptorsSetup = false;
|
||||
|
||||
if (this.__config.languageHeader) {
|
||||
// @ts-ignore butchered something here..
|
||||
this.requestInterceptors.push(addAcceptLanguageHeaderInterceptorFactory(this.__config.lang));
|
||||
}
|
||||
|
||||
if (this.__config.cancelable) {
|
||||
// @ts-ignore butchered something here..
|
||||
this.requestInterceptors.push(cancelInterceptorFactory(this));
|
||||
}
|
||||
|
||||
if (this.__config.cancelPreviousOnNewRequest) {
|
||||
// @ts-ignore butchered something here..
|
||||
this.requestInterceptors.push(cancelPreviousOnNewRequestInterceptorFactory());
|
||||
}
|
||||
|
||||
if (this.__config.jsonPrefix) {
|
||||
const transformer = jsonPrefixTransformerFactory(this.__config.jsonPrefix);
|
||||
this.responseDataTransformers.push(transformer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the config for the instance
|
||||
* @param {AjaxConfig} config configuration for the AjaxClass instance
|
||||
*/
|
||||
set options(config) {
|
||||
// @ts-ignore butchered something here..
|
||||
this.__config = config;
|
||||
}
|
||||
|
||||
get options() {
|
||||
// @ts-ignore butchered something here..
|
||||
return this.__config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a request
|
||||
* @see https://github.com/axios/axios
|
||||
* @param {string} url
|
||||
* @param {{[key:string]: ?}} [config] the config specific for this request
|
||||
* @returns {?}
|
||||
*/
|
||||
request(url, config) {
|
||||
return this.proxy.request.apply(this, [url, { ...this.__config, ...config }]);
|
||||
}
|
||||
|
||||
/** @param {string} msg */
|
||||
// eslint-disable-next-line class-methods-use-this, no-unused-vars
|
||||
cancel(msg) {}
|
||||
|
||||
/**
|
||||
* Dispatches a {@link AxiosRequestConfig} with method 'get' predefined
|
||||
* @param {string} url the endpoint location
|
||||
* @param {{[key:string]: ?}} [config] the config specific for this request
|
||||
* @returns {?}
|
||||
*/
|
||||
get(url, config) {
|
||||
return this.proxy.get.apply(this, [url, { ...this.__config, ...config }]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a {@link AxiosRequestConfig} with method 'delete' predefined
|
||||
* @param {string} url the endpoint location
|
||||
* @param {{[key:string]: ?}} [config] the config specific for this request
|
||||
* @returns {?}
|
||||
*/
|
||||
delete(url, config) {
|
||||
return this.proxy.delete.apply(this, [url, { ...this.__config, ...config }]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a {@link AxiosRequestConfig} with method 'head' predefined
|
||||
* @param {string} url the endpoint location
|
||||
* @param {{[key:string]: ?}} [config] the config specific for this request
|
||||
* @returns {?}
|
||||
*/
|
||||
head(url, config) {
|
||||
return this.proxy.head.apply(this, [url, { ...this.__config, ...config }]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a {@link AxiosRequestConfig} with method 'options' predefined
|
||||
* @param {string} url the endpoint location
|
||||
* @param {{[key:string]: ?}} [config] the config specific for this request
|
||||
* @returns {?}
|
||||
*/
|
||||
// options(url, config) {
|
||||
// return this.proxy.options.apply(this, [url, { ...this.__config, ...config }]);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Dispatches a {@link AxiosRequestConfig} with method 'post' predefined
|
||||
* @param {string} url the endpoint location
|
||||
* @param {Object} [data] the data to be sent to the endpoint
|
||||
* @param {{[key:string]: ?}} [config] the config specific for this request
|
||||
* @returns {?}
|
||||
*/
|
||||
post(url, data, config) {
|
||||
return this.proxy.post.apply(this, [url, data, { ...this.__config, ...config }]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a {@link AxiosRequestConfig} with method 'put' predefined
|
||||
* @param {string} url the endpoint location
|
||||
* @param {Object} [data] the data to be sent to the endpoint
|
||||
* @param {{[key:string]: ?}} [config] the config specific for this request
|
||||
* @returns {?}
|
||||
*/
|
||||
put(url, data, config) {
|
||||
return this.proxy.put.apply(this, [url, data, { ...this.__config, ...config }]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a {@link AxiosRequestConfig} with method 'patch' predefined
|
||||
* @see https://github.com/axios/axios (Request Config)
|
||||
* @param {string} url the endpoint location
|
||||
* @param {Object} [data] the data to be sent to the endpoint
|
||||
* @param {Object} [config] the config specific for this request.
|
||||
* @returns {?}
|
||||
*/
|
||||
patch(url, data, config) {
|
||||
return this.proxy.patch.apply(this, [url, data, { ...this.__config, ...config }]);
|
||||
}
|
||||
|
||||
__setupInterceptors() {
|
||||
this.proxy.interceptors.request.use(
|
||||
/** @param {{[key:string]: unknown}} config */ config => {
|
||||
const configWithTransformers = this.__setupTransformers(config);
|
||||
// @ts-ignore I dont know....
|
||||
return this.requestInterceptors.reduce((c, i) => i(c), configWithTransformers);
|
||||
},
|
||||
/** @param {Error} error */ error => {
|
||||
this.requestErrorInterceptors.forEach(i => i(error));
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
this.proxy.interceptors.response.use(
|
||||
/**
|
||||
* @param {Response} response
|
||||
*/
|
||||
response => this.responseInterceptors.reduce((r, i) => i(r), response),
|
||||
/** @param {Error} error */ error => {
|
||||
this.responseErrorInterceptors.forEach(i => i(error));
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {{[key:string]: ?}} config */
|
||||
__setupTransformers(config) {
|
||||
const axiosTransformRequest = config.transformRequest[0];
|
||||
const axiosTransformResponse = config.transformResponse[0];
|
||||
return {
|
||||
...config,
|
||||
/**
|
||||
* @param {string} data
|
||||
* @param {{[key:string]: ?}} headers
|
||||
*/
|
||||
transformRequest: (data, headers) => {
|
||||
try {
|
||||
const ourData = this.requestDataTransformers.reduce((d, t) => t(d, headers), data);
|
||||
// axios does a lot of smart things with the request that people rely on
|
||||
// and must be the last request data transformer to do this job
|
||||
return axiosTransformRequest(ourData, headers);
|
||||
} catch (error) {
|
||||
this.requestDataErrorTransformers.forEach(t => t(error));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {string} data
|
||||
*/
|
||||
transformResponse: data => {
|
||||
try {
|
||||
// axios does a lot of smart things with the response that people rely on
|
||||
// and must be the first response data transformer to do this job
|
||||
const axiosData = axiosTransformResponse(data);
|
||||
return this.responseDataTransformers.reduce((d, t) => t(d), axiosData);
|
||||
} catch (error) {
|
||||
this.responseDataErrorTransformers.forEach(t => t(error));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
174
packages/ajax/src/AjaxClient.js
Normal file
174
packages/ajax/src/AjaxClient.js
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/* eslint-disable consistent-return */
|
||||
import { acceptLanguageRequestInterceptor, createXSRFRequestInterceptor } from './interceptors.js';
|
||||
import { AjaxClientFetchError } from './AjaxClientFetchError.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} AjaxClientConfig configuration for the AjaxClient instance
|
||||
* @property {boolean} [addAcceptLanguage] the Accept-Language request HTTP header advertises
|
||||
* which languages the client is able to understand, and which locale variant is preferred.
|
||||
* @property {string|null} [xsrfCookieName] name of the XSRF cookie to read from
|
||||
* @property {string|null} [xsrfHeaderName] name of the XSRF header to set
|
||||
* @property {string} [jsonPrefix] the json prefix to use when fetching json (if any)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Intercepts a Request before fetching. Must return an instance of Request or Response.
|
||||
* If a Respone is returned, the network call is skipped and it is returned as is.
|
||||
* @typedef {(request: Request) => Promise<Request | Response>} RequestInterceptor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Intercepts a Response before returning. Must return an instance of Response.
|
||||
* @typedef {(response: Response) => Promise<Response>} ResponseInterceptor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Overrides the body property to also allow javascript objects
|
||||
* as they get string encoded automatically
|
||||
* @typedef {import('../types/ajaxClientTypes').LionRequestInit} LionRequestInit
|
||||
*/
|
||||
|
||||
/**
|
||||
* HTTP Client which acts as a small wrapper around `fetch`. Allows registering hooks which
|
||||
* intercept request and responses, for example to add authorization headers or logging. A
|
||||
* request can also be prevented from reaching the network at all by returning the Response directly.
|
||||
*/
|
||||
export class AjaxClient {
|
||||
/**
|
||||
* @param {AjaxClientConfig} config
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
const {
|
||||
addAcceptLanguage = true,
|
||||
xsrfCookieName = 'XSRF-TOKEN',
|
||||
xsrfHeaderName = 'X-XSRF-TOKEN',
|
||||
jsonPrefix,
|
||||
} = config;
|
||||
|
||||
/** @type {string | undefined} */
|
||||
this._jsonPrefix = jsonPrefix;
|
||||
/** @type {RequestInterceptor[]} */
|
||||
this._requestInterceptors = [];
|
||||
/** @type {ResponseInterceptor[]} */
|
||||
this._responseInterceptors = [];
|
||||
|
||||
if (addAcceptLanguage) {
|
||||
this.addRequestInterceptor(acceptLanguageRequestInterceptor);
|
||||
}
|
||||
|
||||
if (xsrfCookieName && xsrfHeaderName) {
|
||||
this.addRequestInterceptor(createXSRFRequestInterceptor(xsrfCookieName, xsrfHeaderName));
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {RequestInterceptor} requestInterceptor */
|
||||
addRequestInterceptor(requestInterceptor) {
|
||||
this._requestInterceptors.push(requestInterceptor);
|
||||
}
|
||||
|
||||
/** @param {RequestInterceptor} requestInterceptor */
|
||||
removeRequestInterceptor(requestInterceptor) {
|
||||
const indexOf = this._requestInterceptors.indexOf(requestInterceptor);
|
||||
if (indexOf !== -1) {
|
||||
this._requestInterceptors.splice(indexOf);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {ResponseInterceptor} responseInterceptor */
|
||||
addResponseInterceptor(responseInterceptor) {
|
||||
this._responseInterceptors.push(responseInterceptor);
|
||||
}
|
||||
|
||||
/** @param {ResponseInterceptor} responseInterceptor */
|
||||
removeResponseInterceptor(responseInterceptor) {
|
||||
const indexOf = this._responseInterceptors.indexOf(responseInterceptor);
|
||||
if (indexOf !== -1) {
|
||||
this._responseInterceptors.splice(indexOf, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a fetch request, calling the registered fetch request and response
|
||||
* interceptors.
|
||||
*
|
||||
* @param {RequestInfo} info
|
||||
* @param {RequestInit} [init]
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async request(info, init) {
|
||||
const request = new Request(info, init);
|
||||
|
||||
// run request interceptors, returning directly and skipping the network
|
||||
// if a interceptor returns a Response
|
||||
let interceptedRequest = request;
|
||||
for (const intercept of this._requestInterceptors) {
|
||||
// In this instance we actually do want to await for each sequence
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const interceptedRequestOrResponse = await intercept(interceptedRequest);
|
||||
if (interceptedRequestOrResponse instanceof Request) {
|
||||
interceptedRequest = interceptedRequestOrResponse;
|
||||
} else {
|
||||
return interceptedRequestOrResponse;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(interceptedRequest);
|
||||
|
||||
let interceptedResponse = response;
|
||||
for (const intercept of this._responseInterceptors) {
|
||||
// In this instance we actually do want to await for each sequence
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
interceptedResponse = await intercept(interceptedResponse);
|
||||
}
|
||||
|
||||
if (interceptedResponse.status >= 400 && interceptedResponse.status < 600) {
|
||||
throw new AjaxClientFetchError(request, interceptedResponse);
|
||||
}
|
||||
return interceptedResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a fetch request, calling the registered fetch request and response
|
||||
* interceptors. Encodes/decodes the request and response body as JSON.
|
||||
*
|
||||
* @param {RequestInfo} info
|
||||
* @param {LionRequestInit} [init]
|
||||
* @template T
|
||||
* @returns {Promise<{ response: Response, body: T }>}
|
||||
*/
|
||||
async requestJson(info, init) {
|
||||
const lionInit = {
|
||||
...init,
|
||||
headers: {
|
||||
...(init && init.headers),
|
||||
accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
if (lionInit && lionInit.body) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
lionInit.headers['content-type'] = 'application/json';
|
||||
lionInit.body = JSON.stringify(lionInit.body);
|
||||
}
|
||||
|
||||
// Now that we stringified lionInit.body, we can safely typecast LionRequestInit back to RequestInit
|
||||
const jsonInit = /** @type {RequestInit} */ (lionInit);
|
||||
const response = await this.request(info, jsonInit);
|
||||
let responseText = await response.text();
|
||||
|
||||
if (typeof this._jsonPrefix === 'string') {
|
||||
if (responseText.startsWith(this._jsonPrefix)) {
|
||||
responseText = responseText.substring(this._jsonPrefix.length);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
response,
|
||||
body: JSON.parse(responseText),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse response from ${response.url} as JSON.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
export class HttpClientFetchError extends Error {
|
||||
export class AjaxClientFetchError extends Error {
|
||||
/**
|
||||
* @param {Request} request
|
||||
* @param {Response} response
|
||||
*/
|
||||
constructor(request, response) {
|
||||
super(`Fetch request to ${request.url} failed.`);
|
||||
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
}
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
import { singletonManager } from 'singleton-manager';
|
||||
import { AjaxClass } from './AjaxClass.js';
|
||||
import { AjaxClient } from './AjaxClient.js';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export let ajax = singletonManager.get('@lion/ajax::ajax::0.3.x') || new AjaxClass(); // eslint-disable-line import/no-mutable-exports
|
||||
export let ajax = new AjaxClient(); // eslint-disable-line import/no-mutable-exports
|
||||
|
||||
/**
|
||||
* setAjax allows the Application Developer to override the globally used instance of {@link:ajax}.
|
||||
* All interactions with {@link:ajax} after the call to setAjax will use this new instance
|
||||
* (so make sure to call this method before dependant code using {@link:ajax} is ran and this
|
||||
* method is not called by any of your (indirect) dependencies.)
|
||||
* @param {AjaxClass} newAjax the globally used instance of {@link:ajax}.
|
||||
* @param {AjaxClient} newAjax the globally used instance of {@link:ajax}.
|
||||
*/
|
||||
export function setAjax(newAjax) {
|
||||
ajax = newAjax;
|
||||
|
|
|
|||
|
|
@ -1,57 +1,50 @@
|
|||
// @ts-ignore no types for bundled-es-modules/axios
|
||||
import { axios } from '@bundled-es-modules/axios';
|
||||
import { localize } from '@lion/localize';
|
||||
|
||||
/**
|
||||
* @param {string} [lang]
|
||||
* @return {(config: {[key:string]: ?}) => {[key:string]: ?}}
|
||||
* @typedef {import('./AjaxClient').RequestInterceptor} RequestInterceptor
|
||||
*/
|
||||
export function addAcceptLanguageHeaderInterceptorFactory(lang) {
|
||||
return /** @param {{[key:string]: ?}} config */ config => {
|
||||
const result = config;
|
||||
if (typeof lang === 'string' && lang !== '') {
|
||||
if (typeof result.headers !== 'object') {
|
||||
result.headers = {};
|
||||
}
|
||||
const withLang = { headers: { 'Accept-Language': lang, ...result.headers } };
|
||||
return { ...result, ...withLang };
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} name the cookie name
|
||||
* @param {Document | { cookie: string }} _document overwriteable for testing
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export function getCookie(name, _document = document) {
|
||||
const match = _document.cookie.match(new RegExp(`(^|;\\s*)(${name})=([^;]*)`));
|
||||
return match ? decodeURIComponent(match[3]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./AjaxClass').AjaxClass} ajaxInstance
|
||||
* @return {(config: {[key:string]: ?}) => {[key:string]: ?}}
|
||||
* Transforms a request, adding an accept-language header with the current application's locale
|
||||
* if it has not already been set.
|
||||
* @type {RequestInterceptor}
|
||||
*/
|
||||
export function cancelInterceptorFactory(ajaxInstance) {
|
||||
/** @type {unknown[]} */
|
||||
const cancelSources = [];
|
||||
return /** @param {{[key:string]: ?}} config */ config => {
|
||||
const source = axios.CancelToken.source();
|
||||
cancelSources.push(source);
|
||||
/* eslint-disable-next-line no-param-reassign */
|
||||
ajaxInstance.cancel = (message = 'Operation canceled by the user.') => {
|
||||
// @ts-ignore axios is untyped so we don't know the type for the source
|
||||
cancelSources.forEach(s => s.cancel(message));
|
||||
};
|
||||
return { ...config, cancelToken: source.token };
|
||||
};
|
||||
export async function acceptLanguageRequestInterceptor(request) {
|
||||
if (!request.headers.has('accept-language')) {
|
||||
request.headers.set('accept-language', localize.locale);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {(config: {[key:string]: ?}) => {[key:string]: ?}}
|
||||
* Creates a request transformer that adds a XSRF header for protecting
|
||||
* against cross-site request forgery.
|
||||
* @param {string} cookieName the cookie name
|
||||
* @param {string} headerName the header name
|
||||
* @param {Document | { cookie: string }} _document overwriteable for testing
|
||||
* @returns {RequestInterceptor}
|
||||
*/
|
||||
export function cancelPreviousOnNewRequestInterceptorFactory() {
|
||||
// @ts-ignore axios is untyped so we don't know the type for the source
|
||||
let prevCancelSource;
|
||||
return /** @param {{[key:string]: ?}} config */ config => {
|
||||
// @ts-ignore axios is untyped so we don't know the type for the source
|
||||
if (prevCancelSource) {
|
||||
// @ts-ignore
|
||||
prevCancelSource.cancel('Concurrent requests not allowed.');
|
||||
export function createXSRFRequestInterceptor(cookieName, headerName, _document = document) {
|
||||
/**
|
||||
* @type {RequestInterceptor}
|
||||
*/
|
||||
async function xsrfRequestInterceptor(request) {
|
||||
const xsrfToken = getCookie(cookieName, _document);
|
||||
if (xsrfToken) {
|
||||
request.headers.set(headerName, xsrfToken);
|
||||
}
|
||||
const source = axios.CancelToken.source();
|
||||
prevCancelSource = source;
|
||||
return { ...config, cancelToken: source.token };
|
||||
};
|
||||
return request;
|
||||
}
|
||||
|
||||
return xsrfRequestInterceptor;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
/**
|
||||
* @param {string} prefix
|
||||
*/
|
||||
export function jsonPrefixTransformerFactory(prefix) {
|
||||
return /** @param {string} data */ data => {
|
||||
let result = data;
|
||||
if (typeof result === 'string') {
|
||||
if (prefix.length > 0 && result.indexOf(prefix) === 0) {
|
||||
result = result.substring(prefix.length);
|
||||
}
|
||||
try {
|
||||
result = JSON.parse(result);
|
||||
} catch (e) {
|
||||
/* ignore to allow non-JSON responses */
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
import { expect } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { AjaxClass } from '../src/AjaxClass.js';
|
||||
|
||||
describe('AjaxClass interceptors', () => {
|
||||
/** @type {import('sinon').SinonFakeServer} */
|
||||
let server;
|
||||
|
||||
/**
|
||||
* @param {Object} [cfg] configuration for the AjaxClass instance
|
||||
* @param {string} cfg.jsonPrefix prefixing the JSON string in this manner is used to help
|
||||
* prevent JSON Hijacking. The prefix renders the string syntactically invalid as a script so
|
||||
* that it cannot be hijacked. This prefix should be stripped before parsing the string as JSON.
|
||||
* @param {string} cfg.lang language
|
||||
* @param {boolean} cfg.languageHeader the Accept-Language request HTTP header advertises
|
||||
* which languages the client is able to understand, and which locale variant is preferred.
|
||||
* @param {boolean} cfg.cancelable if request can be canceled
|
||||
* @param {boolean} cfg.cancelPreviousOnNewRequest prevents concurrent requests
|
||||
*/
|
||||
function getInstance(cfg) {
|
||||
return new AjaxClass(cfg);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
server = sinon.fakeServer.create({ autoRespond: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('use cases', () => {
|
||||
it('can be added on a class for all instances', () => {
|
||||
['requestInterceptors', 'responseInterceptors'].forEach(type => {
|
||||
const myInterceptor = () => {};
|
||||
class MyApi extends AjaxClass {
|
||||
constructor() {
|
||||
super();
|
||||
this[type] = [...this[type], myInterceptor];
|
||||
}
|
||||
}
|
||||
const ajaxWithout = getInstance();
|
||||
const ajaxWith = new MyApi();
|
||||
expect(ajaxWithout[type]).to.not.include(myInterceptor);
|
||||
expect(ajaxWith[type]).to.include(myInterceptor);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be added per instance without changing the class', () => {
|
||||
['requestInterceptors', 'responseInterceptors'].forEach(type => {
|
||||
const myInterceptor = () => {};
|
||||
const ajaxWithout = getInstance();
|
||||
const ajaxWith = getInstance();
|
||||
ajaxWith[type].push(myInterceptor);
|
||||
expect(ajaxWithout[type]).to.not.include(myInterceptor);
|
||||
expect(ajaxWith[type]).to.include(myInterceptor);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be removed after request', async () => {
|
||||
await Promise.all(
|
||||
['requestInterceptors', 'responseInterceptors'].map(async type => {
|
||||
server.respondWith('GET', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{}',
|
||||
]);
|
||||
|
||||
const myInterceptor = sinon.spy(foo => foo);
|
||||
|
||||
const ajax = getInstance();
|
||||
|
||||
ajax[type].push(myInterceptor);
|
||||
await ajax.get('data.json');
|
||||
|
||||
ajax[type] = ajax[type].filter(/** @param {?} item */ item => item !== myInterceptor);
|
||||
await ajax.get('data.json');
|
||||
|
||||
expect(myInterceptor.callCount).to.eql(1);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('has access to provided instance config(options) on requestInterceptors', async () => {
|
||||
server.respondWith('GET', 'data.json', [200, { 'Content-Type': 'application/json' }, '{}']);
|
||||
const ajax = getInstance();
|
||||
// @ts-ignore setting a prop that isn't existing on options
|
||||
ajax.options.myCustomValue = 'foo';
|
||||
let customValueAccess = false;
|
||||
const myInterceptor = /** @param {{[key: string]: ?}} config */ config => {
|
||||
customValueAccess = config.myCustomValue === 'foo';
|
||||
return config;
|
||||
};
|
||||
// @ts-ignore butchered something here..
|
||||
ajax.requestInterceptors.push(myInterceptor);
|
||||
await ajax.get('data.json');
|
||||
expect(customValueAccess).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestInterceptors', () => {
|
||||
it('allow to intercept request to change config', async () => {
|
||||
server.respondWith('POST', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "method": "post" }',
|
||||
]);
|
||||
server.respondWith('PUT', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "method": "put" }',
|
||||
]);
|
||||
const enforcePutInterceptor = /** @param {{[key: string]: ?}} config */ config => ({
|
||||
...config,
|
||||
method: 'PUT',
|
||||
});
|
||||
const myAjax = getInstance();
|
||||
// @ts-ignore butchered something here..
|
||||
myAjax.requestInterceptors.push(enforcePutInterceptor);
|
||||
const response = await myAjax.post('data.json');
|
||||
expect(response.data).to.deep.equal({ method: 'put' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('responseInterceptors', () => {
|
||||
it('allow to intercept response to change data', async () => {
|
||||
server.respondWith('GET', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "method": "get" }',
|
||||
]);
|
||||
const addDataInterceptor = /** @param {{[key: string]: ?}} config */ config => ({
|
||||
...config,
|
||||
data: { ...config.data, foo: 'bar' },
|
||||
});
|
||||
const myAjax = getInstance();
|
||||
// @ts-ignore I probably butchered the types here or adding data like above is simply not allowed in Response objects
|
||||
myAjax.responseInterceptors.push(addDataInterceptor);
|
||||
const response = await myAjax.get('data.json');
|
||||
expect(response.data).to.deep.equal({ method: 'get', foo: 'bar' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
import { expect, aTimeout } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { AjaxClass } from '../src/AjaxClass.js';
|
||||
|
||||
describe('AjaxClass languages', () => {
|
||||
/** @type {import('sinon').SinonFakeXMLHttpRequestStatic} */
|
||||
let fakeXhr;
|
||||
/** @type {import('sinon').SinonFakeXMLHttpRequest[]} */
|
||||
let requests;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeXhr = sinon.useFakeXMLHttpRequest();
|
||||
requests = [];
|
||||
fakeXhr.onCreate = xhr => {
|
||||
requests.push(xhr);
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fakeXhr.restore();
|
||||
document.documentElement.lang = 'en-GB';
|
||||
});
|
||||
|
||||
it('sets "Accept-Language" header to "en-GB" for one request if <html lang="en-GB">', async () => {
|
||||
document.documentElement.lang = 'en-GB';
|
||||
const req = new AjaxClass();
|
||||
req.get('data.json');
|
||||
await aTimeout(0);
|
||||
expect(requests.length).to.equal(1);
|
||||
expect(requests[0].requestHeaders['Accept-Language']).to.equal('en-GB');
|
||||
});
|
||||
|
||||
it('sets "Accept-Language" header to "en-GB" for multiple subsequent requests if <html lang="en-GB">', async () => {
|
||||
document.documentElement.lang = 'en-GB';
|
||||
const req = new AjaxClass();
|
||||
req.get('data1.json');
|
||||
req.post('data2.json');
|
||||
req.put('data3.json');
|
||||
req.delete('data4.json');
|
||||
await aTimeout(0);
|
||||
expect(requests.length).to.equal(4);
|
||||
requests.forEach(request => {
|
||||
expect(request.requestHeaders['Accept-Language']).to.equal('en-GB');
|
||||
});
|
||||
});
|
||||
|
||||
it('sets "Accept-Language" header to "nl-NL" for one request if <html lang="nl-NL">', async () => {
|
||||
document.documentElement.lang = 'nl-NL';
|
||||
const req = new AjaxClass();
|
||||
req.get('data.json');
|
||||
await aTimeout(0);
|
||||
expect(requests.length).to.equal(1);
|
||||
expect(requests[0].requestHeaders['Accept-Language']).to.equal('nl-NL');
|
||||
});
|
||||
|
||||
it('sets "Accept-Language" header to "nl-NL" for multiple subsequent requests if <html lang="nl-NL">', async () => {
|
||||
document.documentElement.lang = 'nl-NL';
|
||||
const req = new AjaxClass();
|
||||
req.get('data1.json');
|
||||
req.post('data2.json');
|
||||
req.put('data3.json');
|
||||
req.delete('data4.json');
|
||||
await aTimeout(0);
|
||||
expect(requests.length).to.equal(4);
|
||||
requests.forEach(request => {
|
||||
expect(request.requestHeaders['Accept-Language']).to.equal('nl-NL');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not set "Accept-Language" header if <html lang="">', async () => {
|
||||
document.documentElement.lang = '';
|
||||
const req = new AjaxClass();
|
||||
req.get('data.json');
|
||||
await aTimeout(0);
|
||||
expect(requests.length).to.equal(1);
|
||||
expect(requests[0].requestHeaders['Accept-Language']).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
import { expect } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { AjaxClass } from '../src/AjaxClass.js';
|
||||
import { ajax } from '../src/ajax.js';
|
||||
|
||||
describe('AjaxClass', () => {
|
||||
/** @type {import('sinon').SinonFakeServer} */
|
||||
let server;
|
||||
|
||||
/**
|
||||
* @param {Object} [cfg] configuration for the AjaxClass instance
|
||||
* @param {string} [cfg.jsonPrefix] prefixing the JSON string in this manner is used to help
|
||||
* prevent JSON Hijacking. The prefix renders the string syntactically invalid as a script so
|
||||
* that it cannot be hijacked. This prefix should be stripped before parsing the string as JSON.
|
||||
* @param {string} [cfg.lang] language
|
||||
* @param {boolean} [cfg.languageHeader] the Accept-Language request HTTP header advertises
|
||||
* which languages the client is able to understand, and which locale variant is preferred.
|
||||
* @param {boolean} [cfg.cancelable] if request can be canceled
|
||||
* @param {boolean} [cfg.cancelPreviousOnNewRequest] prevents concurrent requests
|
||||
*/
|
||||
function getInstance(cfg) {
|
||||
return new AjaxClass(cfg);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
server = sinon.fakeServer.create({ autoRespond: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
it('sets content type json if passed an object', async () => {
|
||||
const myAjax = getInstance();
|
||||
server.respondWith('POST', /\/api\/foo/, [200, { 'Content-Type': 'application/json' }, '']);
|
||||
await myAjax.post('/api/foo', { a: 1, b: 2 });
|
||||
expect(server.requests[0].requestHeaders['Content-Type']).to.include('application/json');
|
||||
});
|
||||
|
||||
describe('AjaxClass({ jsonPrefix: "%prefix%" })', () => {
|
||||
it('adds new transformer to responseDataTransformers', () => {
|
||||
const myAjaxWithout = getInstance({ jsonPrefix: '' });
|
||||
const myAjaxWith = getInstance({ jsonPrefix: 'prefix' });
|
||||
const lengthWithout = myAjaxWithout.responseDataTransformers.length;
|
||||
const lengthWith = myAjaxWith.responseDataTransformers.length;
|
||||
expect(lengthWith - lengthWithout).to.eql(1);
|
||||
});
|
||||
|
||||
it('allows to customize anti-XSSI prefix', async () => {
|
||||
server.respondWith('GET', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'for(;;);{"success":true}',
|
||||
]);
|
||||
|
||||
const myAjax = getInstance({ jsonPrefix: 'for(;;);' });
|
||||
const response = await myAjax.get('data.json');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.data.success).to.equal(true);
|
||||
});
|
||||
|
||||
it('works with non-JSON responses', async () => {
|
||||
server.respondWith('GET', 'data.txt', [200, { 'Content-Type': 'text/plain' }, 'some text']);
|
||||
|
||||
const myAjax = getInstance({ jsonPrefix: 'for(;;);' });
|
||||
const response = await myAjax.get('data.txt');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.data).to.equal('some text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AjaxClass({ cancelable: true })', () => {
|
||||
it('adds new interceptor to requestInterceptors', () => {
|
||||
const myAjaxWithout = getInstance();
|
||||
const myAjaxWith = getInstance({ cancelable: true });
|
||||
const lengthWithout = myAjaxWithout.requestInterceptors.length;
|
||||
const lengthWith = myAjaxWith.requestInterceptors.length;
|
||||
expect(lengthWith - lengthWithout).to.eql(1);
|
||||
});
|
||||
|
||||
it('allows to cancel single running requests', async () => {
|
||||
const myAjax = getInstance({ cancelable: true });
|
||||
|
||||
setTimeout(() => {
|
||||
myAjax.cancel('is cancelled');
|
||||
});
|
||||
|
||||
try {
|
||||
await myAjax.get('data.json');
|
||||
throw new Error('is not cancelled');
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal('is cancelled');
|
||||
}
|
||||
});
|
||||
|
||||
it('allows to cancel multiple running requests', async () => {
|
||||
const myAjax = getInstance({ cancelable: true });
|
||||
let cancelCount = 0;
|
||||
|
||||
setTimeout(() => {
|
||||
myAjax.cancel('is cancelled');
|
||||
});
|
||||
|
||||
const makeRequest = async () => {
|
||||
try {
|
||||
await myAjax.get('data.json');
|
||||
throw new Error('is not cancelled');
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal('is cancelled');
|
||||
cancelCount += 1;
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([makeRequest(), makeRequest(), makeRequest()]);
|
||||
|
||||
expect(cancelCount).to.equal(3);
|
||||
});
|
||||
|
||||
it('does not cancel resolved requests', async () => {
|
||||
const myAjax = getInstance({ cancelable: true });
|
||||
server.respondWith('GET', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "method": "get" }',
|
||||
]);
|
||||
|
||||
try {
|
||||
const response = await myAjax.get('data.json');
|
||||
expect(response.data).to.deep.equal({ method: 'get' });
|
||||
myAjax.cancel('is cancelled');
|
||||
} catch (error) {
|
||||
throw new Error('is cancelled');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('AjaxClass({ cancelPreviousOnNewRequest: true })', () => {
|
||||
it('adds new interceptor to requestInterceptors', () => {
|
||||
const myAjaxWithout = getInstance();
|
||||
const myAjaxWith = getInstance({ cancelPreviousOnNewRequest: true });
|
||||
const lengthWithout = myAjaxWithout.requestInterceptors.length;
|
||||
const lengthWith = myAjaxWith.requestInterceptors.length;
|
||||
expect(lengthWith - lengthWithout).to.eql(1);
|
||||
});
|
||||
|
||||
it('automatically cancels previous running request', async () => {
|
||||
const myAjax = getInstance({ cancelPreviousOnNewRequest: true });
|
||||
server.respondWith('GET', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "method": "get" }',
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
try {
|
||||
await myAjax.get('data.json');
|
||||
throw new Error('is resolved');
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal('Concurrent requests not allowed.');
|
||||
}
|
||||
})(),
|
||||
(async () => {
|
||||
try {
|
||||
const response = await myAjax.get('data.json');
|
||||
expect(response.data).to.deep.equal({ method: 'get' });
|
||||
} catch (error) {
|
||||
throw new Error('is not resolved');
|
||||
}
|
||||
})(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('automatically cancels multiple previous requests to the same endpoint', async () => {
|
||||
const myAjax = getInstance({ cancelPreviousOnNewRequest: true });
|
||||
server.respondWith('GET', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "method": "get" }',
|
||||
]);
|
||||
|
||||
const makeRequest = async () => {
|
||||
try {
|
||||
await myAjax.get('data.json');
|
||||
throw new Error('is resolved');
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal('Concurrent requests not allowed.');
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
makeRequest(),
|
||||
makeRequest(),
|
||||
makeRequest(),
|
||||
(async () => {
|
||||
try {
|
||||
const response = await myAjax.get('data.json');
|
||||
expect(response.data).to.deep.equal({ method: 'get' });
|
||||
} catch (error) {
|
||||
throw new Error('is not resolved');
|
||||
}
|
||||
})(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('automatically cancels multiple previous requests to different endpoints', async () => {
|
||||
const myAjax = getInstance({ cancelPreviousOnNewRequest: true });
|
||||
server.respondWith('GET', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "method": "get" }',
|
||||
]);
|
||||
|
||||
const makeRequest = /** @param {string} url */ async url => {
|
||||
try {
|
||||
await myAjax.get(url);
|
||||
throw new Error('is resolved');
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal('Concurrent requests not allowed.');
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
makeRequest('data1.json'),
|
||||
makeRequest('data2.json'),
|
||||
makeRequest('data3.json'),
|
||||
(async () => {
|
||||
try {
|
||||
const response = await myAjax.get('data.json');
|
||||
expect(response.data).to.deep.equal({ method: 'get' });
|
||||
} catch (error) {
|
||||
throw new Error('is not resolved');
|
||||
}
|
||||
})(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not automatically cancel requests made via generic ajax', async () => {
|
||||
const myAjax = getInstance({ cancelPreviousOnNewRequest: true });
|
||||
server.respondWith('GET', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "method": "get" }',
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
try {
|
||||
await myAjax.get('data.json');
|
||||
throw new Error('is resolved');
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal('Concurrent requests not allowed.');
|
||||
}
|
||||
})(),
|
||||
(async () => {
|
||||
try {
|
||||
const response = await myAjax.get('data.json');
|
||||
expect(response.data).to.deep.equal({ method: 'get' });
|
||||
} catch (error) {
|
||||
throw new Error('is not resolved');
|
||||
}
|
||||
})(),
|
||||
(async () => {
|
||||
try {
|
||||
const response = await ajax.get('data.json');
|
||||
expect(response.data).to.deep.equal({ method: 'get' });
|
||||
} catch (error) {
|
||||
throw new Error('is not resolved');
|
||||
}
|
||||
})(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not automatically cancel requests made via other instances', async () => {
|
||||
const myAjax1 = getInstance({ cancelPreviousOnNewRequest: true });
|
||||
const myAjax2 = getInstance({ cancelPreviousOnNewRequest: true });
|
||||
server.respondWith('GET', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "method": "get" }',
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
try {
|
||||
await myAjax1.get('data.json');
|
||||
throw new Error('is resolved');
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal('Concurrent requests not allowed.');
|
||||
}
|
||||
})(),
|
||||
(async () => {
|
||||
try {
|
||||
const response = await myAjax2.get('data.json');
|
||||
expect(response.data).to.deep.equal({ method: 'get' });
|
||||
} catch (error) {
|
||||
throw new Error('is not resolved');
|
||||
}
|
||||
})(),
|
||||
(async () => {
|
||||
try {
|
||||
const response = await myAjax1.get('data.json');
|
||||
expect(response.data).to.deep.equal({ method: 'get' });
|
||||
} catch (error) {
|
||||
throw new Error('is not resolved');
|
||||
}
|
||||
})(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
import { expect } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { AjaxClass } from '../src/AjaxClass.js';
|
||||
|
||||
describe('AjaxClass transformers', () => {
|
||||
/** @type {import('sinon').SinonFakeServer} */
|
||||
let server;
|
||||
|
||||
/**
|
||||
* @param {Object} [cfg] configuration for the AjaxClass instance
|
||||
* @param {string} [cfg.jsonPrefix] prefixing the JSON string in this manner is used to help
|
||||
* prevent JSON Hijacking. The prefix renders the string syntactically invalid as a script so
|
||||
* that it cannot be hijacked. This prefix should be stripped before parsing the string as JSON.
|
||||
* @param {string} [cfg.lang] language
|
||||
* @param {boolean} [cfg.languageHeader] the Accept-Language request HTTP header advertises
|
||||
* which languages the client is able to understand, and which locale variant is preferred.
|
||||
* @param {boolean} [cfg.cancelable] if request can be canceled
|
||||
* @param {boolean} [cfg.cancelPreviousOnNewRequest] prevents concurrent requests
|
||||
*/
|
||||
function getInstance(cfg) {
|
||||
return new AjaxClass(cfg);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
server = sinon.fakeServer.create({ autoRespond: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('use cases', () => {
|
||||
it('can be added on a class for all instances', () => {
|
||||
['requestDataTransformers', 'responseDataTransformers'].forEach(type => {
|
||||
const myInterceptor = () => {};
|
||||
class MyApi extends AjaxClass {
|
||||
constructor() {
|
||||
super();
|
||||
this[type] = [...this[type], myInterceptor];
|
||||
}
|
||||
}
|
||||
const ajaxWithout = getInstance();
|
||||
const ajaxWith = new MyApi();
|
||||
expect(ajaxWithout[type]).to.not.include(myInterceptor);
|
||||
expect(ajaxWith[type]).to.include(myInterceptor);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be added per instance without changing the class', () => {
|
||||
['requestDataTransformers', 'responseDataTransformers'].forEach(type => {
|
||||
const myInterceptor = () => {};
|
||||
const ajaxWithout = getInstance();
|
||||
const ajaxWith = getInstance();
|
||||
ajaxWith[type].push(myInterceptor);
|
||||
expect(ajaxWithout[type]).to.not.include(myInterceptor);
|
||||
expect(ajaxWith[type]).to.include(myInterceptor);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be removed after request', async () => {
|
||||
await Promise.all(
|
||||
['requestDataTransformers', 'responseDataTransformers'].map(async type => {
|
||||
server.respondWith('GET', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{}',
|
||||
]);
|
||||
|
||||
const myTransformer = sinon.spy(foo => foo);
|
||||
|
||||
const ajax = getInstance();
|
||||
|
||||
ajax[type].push(myTransformer);
|
||||
await ajax.get('data.json');
|
||||
|
||||
ajax[type] = ajax[type].filter(/** @param {?} item */ item => item !== myTransformer);
|
||||
await ajax.get('data.json');
|
||||
|
||||
expect(myTransformer.callCount).to.eql(1);
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestDataTransformers', () => {
|
||||
it('allow to transform request data', async () => {
|
||||
server.respondWith('POST', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "method": "post" }',
|
||||
]);
|
||||
const addBarTransformer = /** @param {?} data */ data => ({ ...data, bar: 'bar' });
|
||||
const myAjax = getInstance();
|
||||
myAjax.requestDataTransformers.push(addBarTransformer);
|
||||
const response = await myAjax.post('data.json', { foo: 'foo' });
|
||||
expect(JSON.parse(response.config.data)).to.deep.equal({
|
||||
foo: 'foo',
|
||||
bar: 'bar',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('responseDataTransformers', () => {
|
||||
it('allow to transform response data', async () => {
|
||||
server.respondWith('GET', 'data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "method": "get" }',
|
||||
]);
|
||||
const addBarTransformer = /** @param {?} data */ data => ({ ...data, bar: 'bar' });
|
||||
const myAjax = getInstance();
|
||||
myAjax.responseDataTransformers.push(addBarTransformer);
|
||||
const response = await myAjax.get('data.json');
|
||||
expect(response.data).to.deep.equal({ method: 'get', bar: 'bar' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
import { expect } from '@open-wc/testing';
|
||||
import { stub } from 'sinon';
|
||||
import { localize } from '@lion/localize';
|
||||
import { HttpClient } from '../src/HttpClient.js';
|
||||
import { HttpClientFetchError } from '../src/HttpClientFetchError.js';
|
||||
import { AjaxClient } from '../src/AjaxClient.js';
|
||||
import { AjaxClientFetchError } from '../src/AjaxClientFetchError.js';
|
||||
|
||||
describe('HttpClient', () => {
|
||||
describe('AjaxClient', () => {
|
||||
/** @type {import('sinon').SinonStub} */
|
||||
let fetchStub;
|
||||
/** @type {HttpClient} */
|
||||
let http;
|
||||
/** @type {AjaxClient} */
|
||||
let ajax;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchStub = stub(window, 'fetch');
|
||||
fetchStub.returns(Promise.resolve('mock response'));
|
||||
http = new HttpClient();
|
||||
ajax = new AjaxClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -22,7 +22,7 @@ describe('HttpClient', () => {
|
|||
|
||||
describe('request()', () => {
|
||||
it('calls fetch with the given args, returning the result', async () => {
|
||||
const response = await http.request('/foo', { method: 'POST' });
|
||||
const response = await ajax.request('/foo', { method: 'POST' });
|
||||
|
||||
expect(fetchStub).to.have.been.calledOnce;
|
||||
const request = fetchStub.getCall(0).args[0];
|
||||
|
|
@ -36,9 +36,9 @@ describe('HttpClient', () => {
|
|||
|
||||
let thrown = false;
|
||||
try {
|
||||
await http.request('/foo');
|
||||
await ajax.request('/foo');
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceOf(HttpClientFetchError);
|
||||
expect(e).to.be.an.instanceOf(AjaxClientFetchError);
|
||||
expect(e.request).to.be.an.instanceOf(Request);
|
||||
expect(e.response).to.be.an.instanceOf(Response);
|
||||
thrown = true;
|
||||
|
|
@ -51,9 +51,9 @@ describe('HttpClient', () => {
|
|||
|
||||
let thrown = false;
|
||||
try {
|
||||
await http.request('/foo');
|
||||
await ajax.request('/foo');
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceOf(HttpClientFetchError);
|
||||
expect(e).to.be.an.instanceOf(AjaxClientFetchError);
|
||||
expect(e.request).to.be.an.instanceOf(Request);
|
||||
expect(e.response).to.be.an.instanceOf(Response);
|
||||
thrown = true;
|
||||
|
|
@ -68,26 +68,26 @@ describe('HttpClient', () => {
|
|||
});
|
||||
|
||||
it('sets json accept header', async () => {
|
||||
await http.requestJson('/foo');
|
||||
await ajax.requestJson('/foo');
|
||||
const request = fetchStub.getCall(0).args[0];
|
||||
expect(request.headers.get('accept')).to.equal('application/json');
|
||||
});
|
||||
|
||||
it('decodes response from json', async () => {
|
||||
fetchStub.returns(Promise.resolve(new Response('{"a":1,"b":2}')));
|
||||
const response = await http.requestJson('/foo');
|
||||
const response = await ajax.requestJson('/foo');
|
||||
expect(response.body).to.eql({ a: 1, b: 2 });
|
||||
});
|
||||
|
||||
describe('given a request body', () => {
|
||||
it('encodes the request body as json', async () => {
|
||||
await http.requestJson('/foo', { method: 'POST', body: { a: 1, b: 2 } });
|
||||
await ajax.requestJson('/foo', { method: 'POST', body: { a: 1, b: 2 } });
|
||||
const request = fetchStub.getCall(0).args[0];
|
||||
expect(await request.text()).to.equal('{"a":1,"b":2}');
|
||||
});
|
||||
|
||||
it('sets json content-type header', async () => {
|
||||
await http.requestJson('/foo', { method: 'POST', body: { a: 1, b: 2 } });
|
||||
await ajax.requestJson('/foo', { method: 'POST', body: { a: 1, b: 2 } });
|
||||
const request = fetchStub.getCall(0).args[0];
|
||||
expect(request.headers.get('content-type')).to.equal('application/json');
|
||||
});
|
||||
|
|
@ -95,70 +95,78 @@ describe('HttpClient', () => {
|
|||
|
||||
describe('given a json prefix', () => {
|
||||
it('strips json prefix from response before decoding', async () => {
|
||||
const localHttp = new HttpClient({ jsonPrefix: '//.,!' });
|
||||
const localAjax = new AjaxClient({ jsonPrefix: '//.,!' });
|
||||
fetchStub.returns(Promise.resolve(new Response('//.,!{"a":1,"b":2}')));
|
||||
const response = await localHttp.requestJson('/foo');
|
||||
const response = await localAjax.requestJson('/foo');
|
||||
expect(response.body).to.eql({ a: 1, b: 2 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('request and response transformers', () => {
|
||||
it('addRequestTransformer() adds a function which transforms the request', async () => {
|
||||
http.addRequestTransformer(r => new Request(`${r.url}/transformed-1`));
|
||||
http.addRequestTransformer(r => new Request(`${r.url}/transformed-2`));
|
||||
describe('request and response interceptors', () => {
|
||||
it('addRequestInterceptor() adds a function which intercepts the request', async () => {
|
||||
ajax.addRequestInterceptor(async r => {
|
||||
return new Request(`${r.url}/intercepted-1`);
|
||||
});
|
||||
ajax.addRequestInterceptor(async r => new Request(`${r.url}/intercepted-2`));
|
||||
|
||||
await http.request('/foo', { method: 'POST' });
|
||||
await ajax.request('/foo', { method: 'POST' });
|
||||
|
||||
const request = fetchStub.getCall(0).args[0];
|
||||
expect(request.url).to.equal(`${window.location.origin}/foo/transformed-1/transformed-2`);
|
||||
expect(request.url).to.equal(`${window.location.origin}/foo/intercepted-1/intercepted-2`);
|
||||
});
|
||||
|
||||
it('addResponseTransformer() adds a function which transforms the response', async () => {
|
||||
http.addResponseTransformer(r => `${r} transformed-1`);
|
||||
http.addResponseTransformer(r => `${r} transformed-2`);
|
||||
it('addResponseInterceptor() adds a function which intercepts the response', async () => {
|
||||
// @ts-expect-error we're mocking the response as a simple promise which returns a string
|
||||
ajax.addResponseInterceptor(r => `${r} intercepted-1`);
|
||||
// @ts-expect-error we're mocking the response as a simple promise which returns a string
|
||||
ajax.addResponseInterceptor(r => `${r} intercepted-2`);
|
||||
|
||||
const response = await http.request('/foo', { method: 'POST' });
|
||||
expect(response).to.equal('mock response transformed-1 transformed-2');
|
||||
const response = await ajax.request('/foo', { method: 'POST' });
|
||||
expect(response).to.equal('mock response intercepted-1 intercepted-2');
|
||||
});
|
||||
|
||||
it('removeRequestTransformer() removes a request transformer', async () => {
|
||||
const transformer = r => new Request(`${r.url}/transformed-1`);
|
||||
http.addRequestTransformer(transformer);
|
||||
http.removeRequestTransformer(transformer);
|
||||
it('removeRequestInterceptor() removes a request interceptor', async () => {
|
||||
const interceptor = /** @param {Request} r */ async r =>
|
||||
new Request(`${r.url}/intercepted-1`);
|
||||
ajax.addRequestInterceptor(interceptor);
|
||||
ajax.removeRequestInterceptor(interceptor);
|
||||
|
||||
await http.request('/foo', { method: 'POST' });
|
||||
await ajax.request('/foo', { method: 'POST' });
|
||||
|
||||
const request = fetchStub.getCall(0).args[0];
|
||||
expect(request.url).to.equal(`${window.location.origin}/foo`);
|
||||
});
|
||||
|
||||
it('removeResponseTransformer() removes a request transformer', async () => {
|
||||
const transformer = r => `${r} transformed-1`;
|
||||
http.addResponseTransformer(transformer);
|
||||
http.removeResponseTransformer(transformer);
|
||||
it('removeResponseInterceptor() removes a request interceptor', async () => {
|
||||
const interceptor = /** @param {Response} r */ r => `${r} intercepted-1`;
|
||||
// @ts-expect-error we're mocking the response as a simple promise which returns a string
|
||||
ajax.addResponseInterceptor(interceptor);
|
||||
// @ts-expect-error we're mocking the response as a simple promise which returns a string
|
||||
ajax.removeResponseInterceptor(interceptor);
|
||||
|
||||
const response = await http.request('/foo', { method: 'POST' });
|
||||
const response = await ajax.request('/foo', { method: 'POST' });
|
||||
expect(response).to.equal('mock response');
|
||||
});
|
||||
});
|
||||
|
||||
describe('accept-language header', () => {
|
||||
it('is set by default based on localize.locale', async () => {
|
||||
await http.request('/foo');
|
||||
await ajax.request('/foo');
|
||||
const request = fetchStub.getCall(0).args[0];
|
||||
expect(request.headers.get('accept-language')).to.equal(localize.locale);
|
||||
});
|
||||
|
||||
it('can be disabled', async () => {
|
||||
const customHttp = new HttpClient({ addAcceptLanguage: false });
|
||||
await customHttp.request('/foo');
|
||||
const customAjax = new AjaxClient({ addAcceptLanguage: false });
|
||||
await customAjax.request('/foo');
|
||||
const request = fetchStub.getCall(0).args[0];
|
||||
expect(request.headers.has('accept-language')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('XSRF token', () => {
|
||||
/** @type {import('sinon').SinonStub} */
|
||||
let cookieStub;
|
||||
beforeEach(() => {
|
||||
cookieStub = stub(document, 'cookie');
|
||||
|
|
@ -170,30 +178,59 @@ describe('HttpClient', () => {
|
|||
});
|
||||
|
||||
it('XSRF token header is set based on cookie', async () => {
|
||||
await http.request('/foo');
|
||||
await ajax.request('/foo');
|
||||
|
||||
const request = fetchStub.getCall(0).args[0];
|
||||
expect(request.headers.get('X-XSRF-TOKEN')).to.equal('1234');
|
||||
});
|
||||
|
||||
it('XSRF behavior can be disabled', async () => {
|
||||
const customHttp = new HttpClient({ xsrfCookieName: null, xsrfHeaderName: null });
|
||||
await customHttp.request('/foo');
|
||||
await http.request('/foo');
|
||||
const customAjax = new AjaxClient({ xsrfCookieName: null, xsrfHeaderName: null });
|
||||
await customAjax.request('/foo');
|
||||
await ajax.request('/foo');
|
||||
|
||||
const request = fetchStub.getCall(0).args[0];
|
||||
expect(request.headers.has('X-XSRF-TOKEN')).to.be.false;
|
||||
});
|
||||
|
||||
it('XSRF token header and cookie can be customized', async () => {
|
||||
const customHttp = new HttpClient({
|
||||
const customAjax = new AjaxClient({
|
||||
xsrfCookieName: 'CSRF-TOKEN',
|
||||
xsrfHeaderName: 'X-CSRF-TOKEN',
|
||||
});
|
||||
await customHttp.request('/foo');
|
||||
await customAjax.request('/foo');
|
||||
|
||||
const request = fetchStub.getCall(0).args[0];
|
||||
expect(request.headers.get('X-CSRF-TOKEN')).to.equal('5678');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Abort', () => {
|
||||
it('support aborting requests with AbortController', async () => {
|
||||
fetchStub.restore();
|
||||
let err;
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
// Have to do a "real" request to be able to abort it and verify that this throws
|
||||
const req = ajax.request(new URL('./foo.json', import.meta.url).pathname, {
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
controller.abort();
|
||||
|
||||
try {
|
||||
await req;
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
const errors = [
|
||||
"Failed to execute 'fetch' on 'Window': The user aborted a request.", // chromium
|
||||
'The operation was aborted. ', // firefox
|
||||
'Request signal is aborted', // webkit
|
||||
];
|
||||
|
||||
expect(errors.includes(err.message)).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,125 +1,15 @@
|
|||
import { expect } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { ajax } from '../src/ajax.js';
|
||||
import { ajax, setAjax } from '../src/ajax.js';
|
||||
import { AjaxClient } from '../src/AjaxClient.js';
|
||||
|
||||
describe('ajax', () => {
|
||||
/** @type {import('sinon').SinonFakeServer} */
|
||||
let server;
|
||||
|
||||
beforeEach(() => {
|
||||
server = sinon.fakeServer.create({ autoRespond: true });
|
||||
it('exports an instance of AjaxClient', () => {
|
||||
expect(ajax).to.be.an.instanceOf(AjaxClient);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
it('interprets Content-Type of the response by default', async () => {
|
||||
server.respondWith('GET', '/path/to/data/', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{ "json": "yes" }',
|
||||
]);
|
||||
|
||||
const response = await ajax.get('/path/to/data/');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.data).to.deep.equal({ json: 'yes' });
|
||||
});
|
||||
|
||||
it('supports signature (url[, config]) for get(), request(), delete(), head()', async () => {
|
||||
server.respondWith('data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{"success": true}',
|
||||
]);
|
||||
const makeRequest = /** @param {string} method */ async method => {
|
||||
const response = await ajax[method]('data.json', { foo: 'bar' });
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.data).to.deep.equal({ success: true });
|
||||
};
|
||||
await Promise.all(['get', 'request', 'delete', 'head'].map(m => makeRequest(m)));
|
||||
});
|
||||
|
||||
it('supports signature (url[, data[, config]]) for post(), put(), patch()', async () => {
|
||||
server.respondWith('data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{"success": true}',
|
||||
]);
|
||||
const makeRequest = /** @param {string} method */ async method => {
|
||||
const response = await ajax[method]('data.json', { data: 'foobar' }, { foo: 'bar' });
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.data).to.deep.equal({ success: true });
|
||||
};
|
||||
await Promise.all(['post', 'put', 'patch'].map(m => makeRequest(m)));
|
||||
});
|
||||
|
||||
it('supports GET, POST, PUT, DELETE, REQUEST, PATCH and HEAD methods with XSRF token', async () => {
|
||||
document.cookie = 'XSRF-TOKEN=test; ';
|
||||
server.respondWith('data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{"success": true}',
|
||||
]);
|
||||
|
||||
const makeRequest = /** @param {string} method */ async method => {
|
||||
const response = await ajax[method]('data.json');
|
||||
expect(response.config.headers['X-XSRF-TOKEN']).to.equal('test');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.data).to.deep.equal({ success: true });
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
['get', 'post', 'put', 'delete', 'request', 'patch', 'head'].map(m => makeRequest(m)),
|
||||
);
|
||||
});
|
||||
|
||||
it('supports GET, POST, PUT, DELETE, REQUEST, PATCH and HEAD methods without XSRF token', async () => {
|
||||
document.cookie = 'XSRF-TOKEN=; ';
|
||||
server.respondWith('data.json', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
'{"success": true}',
|
||||
]);
|
||||
|
||||
const makeRequest = /** @param {string} method */ async method => {
|
||||
const response = await ajax[method]('data.json');
|
||||
expect(response.config.headers['X-XSRF-TOKEN']).to.equal(undefined);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.data).to.deep.equal({ success: true });
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
['get', 'post', 'put', 'delete', 'request', 'patch', 'head'].map(m => makeRequest(m)),
|
||||
);
|
||||
});
|
||||
|
||||
it('supports empty responses', async () => {
|
||||
server.respondWith('GET', 'data.json', [200, { 'Content-Type': 'application/json' }, '']);
|
||||
|
||||
const response = await ajax.get('data.json');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.data).to.equal('');
|
||||
});
|
||||
|
||||
it('supports error responses', async () => {
|
||||
server.respondWith('GET', 'data.json', [500, { 'Content-Type': 'application/json' }, '']);
|
||||
|
||||
try {
|
||||
await ajax.get('data.json');
|
||||
throw new Error('error is not handled');
|
||||
} catch (error) {
|
||||
expect(error).to.be.an.instanceof(Error);
|
||||
expect(error.response.status).to.equal(500);
|
||||
}
|
||||
});
|
||||
|
||||
it('supports non-JSON responses', async () => {
|
||||
server.respondWith('GET', 'data.txt', [200, { 'Content-Type': 'text/plain' }, 'some text']);
|
||||
|
||||
const response = await ajax.get('data.txt');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.data).to.equal('some text');
|
||||
it('can replace ajax with another instance', () => {
|
||||
const newAjax = new AjaxClient();
|
||||
setAjax(newAjax);
|
||||
expect(ajax).to.equal(newAjax);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
1
packages/ajax/test/foo.json
Normal file
1
packages/ajax/test/foo.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { expect } from '@open-wc/testing';
|
||||
import { localize } from '@lion/localize';
|
||||
import {
|
||||
createXSRFRequestTransformer,
|
||||
createXSRFRequestInterceptor,
|
||||
getCookie,
|
||||
acceptLanguageRequestTransformer,
|
||||
} from '../src/transformers.js';
|
||||
acceptLanguageRequestInterceptor,
|
||||
} from '../src/interceptors.js';
|
||||
|
||||
describe('transformers', () => {
|
||||
describe('interceptors', () => {
|
||||
describe('getCookie()', () => {
|
||||
it('returns the cookie value', () => {
|
||||
expect(getCookie('foo', { cookie: 'foo=bar' })).to.equal('bar');
|
||||
|
|
@ -27,36 +27,36 @@ describe('transformers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('acceptLanguageRequestTransformer()', () => {
|
||||
describe('acceptLanguageRequestInterceptor()', () => {
|
||||
it('adds the locale as accept-language header', () => {
|
||||
const request = new Request('/foo/');
|
||||
acceptLanguageRequestTransformer(request);
|
||||
acceptLanguageRequestInterceptor(request);
|
||||
expect(request.headers.get('accept-language')).to.equal(localize.locale);
|
||||
});
|
||||
|
||||
it('does not change an existing accept-language header', () => {
|
||||
const request = new Request('/foo/', { headers: { 'accept-language': 'my-accept' } });
|
||||
acceptLanguageRequestTransformer(request);
|
||||
acceptLanguageRequestInterceptor(request);
|
||||
expect(request.headers.get('accept-language')).to.equal('my-accept');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createXSRFRequestTransformer()', () => {
|
||||
describe('createXSRFRequestInterceptor()', () => {
|
||||
it('adds the xsrf token header to the request', () => {
|
||||
const transformer = createXSRFRequestTransformer('XSRF-TOKEN', 'X-XSRF-TOKEN', {
|
||||
const interceptor = createXSRFRequestInterceptor('XSRF-TOKEN', 'X-XSRF-TOKEN', {
|
||||
cookie: 'XSRF-TOKEN=foo',
|
||||
});
|
||||
const request = new Request('/foo/');
|
||||
transformer(request);
|
||||
interceptor(request);
|
||||
expect(request.headers.get('X-XSRF-TOKEN')).to.equal('foo');
|
||||
});
|
||||
|
||||
it('doesnt set anything if the cookie is not there', () => {
|
||||
const transformer = createXSRFRequestTransformer('XSRF-TOKEN', 'X-XSRF-TOKEN', {
|
||||
const interceptor = createXSRFRequestInterceptor('XSRF-TOKEN', 'X-XSRF-TOKEN', {
|
||||
cookie: 'XXSRF-TOKEN=foo',
|
||||
});
|
||||
const request = new Request('/foo/');
|
||||
transformer(request);
|
||||
interceptor(request);
|
||||
expect(request.headers.get('X-XSRF-TOKEN')).to.equal(null);
|
||||
});
|
||||
});
|
||||
10
packages/ajax/types/ajaxClientTypes.d.ts
vendored
Normal file
10
packages/ajax/types/ajaxClientTypes.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* We have a method requestJson that encodes JS Object to
|
||||
* a string automatically for `body` property.
|
||||
* Sadly, Typescript doesn't allow us to extend RequestInit
|
||||
* and override body prop because it is incompatible, so we
|
||||
* omit it first from the base RequestInit.
|
||||
*/
|
||||
export interface LionRequestInit extends Omit<RequestInit, 'body'> {
|
||||
body?: BodyInit | null | Object;
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
# HTTP
|
||||
|
||||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
`http` is a small wrapper around `fetch` which:
|
||||
|
||||
- Allows globally registering request and response transformers
|
||||
- Throws on 4xx and 5xx status codes
|
||||
- Supports a JSON request which automatically encodes/decodes body request and response payload as JSON
|
||||
- Adds accept-language header to requests based on application language
|
||||
- Adds XSRF header to request if the cookie is present
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
npm i --save @lion/http
|
||||
```
|
||||
|
||||
### Relation to fetch
|
||||
|
||||
`http` delegates all requests to fetch. `http.request` and `http.requestJson` have the same function signature as `window.fetch`, you can use any online resource to learn more about fetch. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) is a great start.
|
||||
|
||||
### Example requests
|
||||
|
||||
#### GET request
|
||||
|
||||
```js
|
||||
import { http } from '@lion/http';
|
||||
|
||||
const response = await http.request('/api/users');
|
||||
const users = await response.json();
|
||||
```
|
||||
|
||||
#### POST request
|
||||
|
||||
```js
|
||||
import { http } from '@lion/http';
|
||||
|
||||
const response = await http.request('/api/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username: 'steve' }),
|
||||
});
|
||||
const newUser = await response.json();
|
||||
```
|
||||
|
||||
### JSON requests
|
||||
|
||||
We usually deal with JSON requests and responses. With `requestJson` you don't need to specifically stringify the request body or parse the response body:
|
||||
|
||||
#### GET JSON request
|
||||
|
||||
```js
|
||||
import { http } from '@lion/http';
|
||||
|
||||
const { response, body } = await http.requestJson('/api/users');
|
||||
```
|
||||
|
||||
#### POST JSON request
|
||||
|
||||
```js
|
||||
import { http } from '@lion/http';
|
||||
|
||||
const { response, body } = await http.requestJson('/api/users', {
|
||||
method: 'POST',
|
||||
body: { username: 'steve' },
|
||||
});
|
||||
```
|
||||
|
||||
### Error handling
|
||||
|
||||
Different from fetch, `http` throws when the server returns a 4xx or 5xx, returning the request and response:
|
||||
|
||||
```js
|
||||
import { http } from '@lion/http';
|
||||
|
||||
try {
|
||||
const users = await http.requestJson('/api/users');
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
if (error.response.status === 400) {
|
||||
// handle a specific status code, for example 400 bad request
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
// an error happened before receiving a response, ex. an incorrect request or network error
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export { http, setHttp } from './src/http.js';
|
||||
export { HttpClient } from './src/HttpClient.js';
|
||||
|
||||
export {
|
||||
acceptLanguageRequestTransformer,
|
||||
createXSRFRequestTransformer,
|
||||
} from './src/transformers.js';
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"name": "@lion/http",
|
||||
"version": "0.0.0",
|
||||
"description": "Thin wrapper around fetch.",
|
||||
"author": "ing-bank",
|
||||
"homepage": "https://github.com/ing-bank/lion/",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ing-bank/lion.git",
|
||||
"directory": "packages/http"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "../../scripts/npm-prepublish.js"
|
||||
},
|
||||
"keywords": [
|
||||
"lion",
|
||||
"web-components",
|
||||
"fetch",
|
||||
"ajax",
|
||||
"http"
|
||||
],
|
||||
"main": "index.js",
|
||||
"module": "index.js",
|
||||
"files": [
|
||||
"docs",
|
||||
"src",
|
||||
"stories",
|
||||
"test",
|
||||
"translations",
|
||||
"*.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@lion/localize": "^0.4.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@open-wc/demoing-storybook": "^0.2.0",
|
||||
"@open-wc/testing": "^2.3.4",
|
||||
"sinon": "^7.2.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
/* eslint-disable consistent-return */
|
||||
import { acceptLanguageRequestTransformer, createXSRFRequestTransformer } from './transformers.js';
|
||||
import { HttpClientFetchError } from './HttpClientFetchError.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} HttpClientConfig configuration for the HttpClient instance
|
||||
* @property {boolean} addAcceptLanguage the Accept-Language request HTTP header advertises
|
||||
* which languages the client is able to understand, and which locale variant is preferred.
|
||||
* @property {string} [xsrfCookieName] name of the XSRF cookie to read from
|
||||
* @property {string} [xsrfHeaderName] name of the XSRF header to set
|
||||
* @property {string} [jsonPrefix] the json prefix to use when fetching json (if any)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transforms a Request before fetching. Must return an instance of Request or Response.
|
||||
* If a Respone is returned, the network call is skipped and it is returned as is.
|
||||
* @typedef {(request: Request) => Request | Response} RequestTransformer
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transforms a Response before returning. Must return an instance of Response.
|
||||
* @typedef {(response: Response) => Response} ResponseTransformer
|
||||
*/
|
||||
|
||||
/**
|
||||
* HTTP Client which acts as a small wrapper around `fetch`. Allows registering hooks which
|
||||
* transform request and responses, for example to add authorization headers or logging. A
|
||||
* request can also be prevented from reaching the network at all by returning the Response directly.
|
||||
*/
|
||||
export class HttpClient {
|
||||
/**
|
||||
* @param {HttpClientConfig} config
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
const {
|
||||
addAcceptLanguage = true,
|
||||
xsrfCookieName = 'XSRF-TOKEN',
|
||||
xsrfHeaderName = 'X-XSRF-TOKEN',
|
||||
jsonPrefix,
|
||||
} = config;
|
||||
|
||||
/** @type {string | undefined} */
|
||||
this._jsonPrefix = jsonPrefix;
|
||||
/** @type {RequestTransformer[]} */
|
||||
this._requestTransformers = [];
|
||||
/** @type {ResponseTransformer[]} */
|
||||
this._responseTransformers = [];
|
||||
|
||||
if (addAcceptLanguage) {
|
||||
this.addRequestTransformer(acceptLanguageRequestTransformer);
|
||||
}
|
||||
|
||||
if (xsrfCookieName && xsrfHeaderName) {
|
||||
this.addRequestTransformer(createXSRFRequestTransformer(xsrfCookieName, xsrfHeaderName));
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {RequestTransformer} requestTransformer */
|
||||
addRequestTransformer(requestTransformer) {
|
||||
this._requestTransformers.push(requestTransformer);
|
||||
}
|
||||
|
||||
/** @param {RequestTransformer} requestTransformer */
|
||||
removeRequestTransformer(requestTransformer) {
|
||||
const indexOf = this._requestTransformers.indexOf(requestTransformer);
|
||||
if (indexOf !== -1) {
|
||||
this._requestTransformers.splice(indexOf);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {ResponseTransformer} responseTransformer */
|
||||
addResponseTransformer(responseTransformer) {
|
||||
this._responseTransformers.push(responseTransformer);
|
||||
}
|
||||
|
||||
/** @param {ResponseTransformer} responseTransformer */
|
||||
removeResponseTransformer(responseTransformer) {
|
||||
const indexOf = this._responseTransformers.indexOf(responseTransformer);
|
||||
if (indexOf !== -1) {
|
||||
this._responseTransformers.splice(indexOf, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a fetch request, calling the registered fetch request and response
|
||||
* transformers.
|
||||
*
|
||||
* @param {RequestInfo} info
|
||||
* @param {RequestInit} [init]
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async request(info, init) {
|
||||
const request = new Request(info, init);
|
||||
/** @type {Request | Response} */
|
||||
let transformedRequestOrResponse = request;
|
||||
|
||||
// run request transformers, returning directly and skipping the network
|
||||
// if a transformer returns a Response
|
||||
this._requestTransformers.forEach(transform => {
|
||||
transformedRequestOrResponse = transform(transformedRequestOrResponse);
|
||||
if (transformedRequestOrResponse instanceof Response) {
|
||||
return transformedRequestOrResponse;
|
||||
}
|
||||
});
|
||||
|
||||
const response = await fetch(transformedRequestOrResponse);
|
||||
const transformedResponse = this._responseTransformers.reduce(
|
||||
(prev, transform) => transform(prev),
|
||||
response,
|
||||
);
|
||||
if (transformedResponse.status >= 400 && transformedResponse.status < 600) {
|
||||
throw new HttpClientFetchError(request, transformedResponse);
|
||||
}
|
||||
return transformedResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a fetch request, calling the registered fetch request and response
|
||||
* transformers. Encodes/decodes the request and response body as JSON.
|
||||
*
|
||||
* @param {RequestInfo} info
|
||||
* @param {RequestInit} [init]
|
||||
* @template T
|
||||
* @returns {Promise<{ response: Response, body: T }>}
|
||||
*/
|
||||
async requestJson(info, init) {
|
||||
const jsonInit = {
|
||||
...init,
|
||||
headers: {
|
||||
...(init && init.headers),
|
||||
accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
if (init && init.body) {
|
||||
jsonInit.headers['content-type'] = 'application/json';
|
||||
jsonInit.body = JSON.stringify(init.body);
|
||||
}
|
||||
|
||||
const response = await this.request(info, jsonInit);
|
||||
let responseText = await response.text();
|
||||
|
||||
if (typeof this._jsonPrefix === 'string') {
|
||||
if (responseText.startsWith(this._jsonPrefix)) {
|
||||
responseText = responseText.substring(this._jsonPrefix.length);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
response,
|
||||
body: JSON.parse(responseText),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse response from ${response.url} as JSON.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { HttpClient } from './HttpClient.js';
|
||||
|
||||
export let http = new HttpClient(); // eslint-disable-line import/no-mutable-exports
|
||||
|
||||
/**
|
||||
* setHttp allows the Application Developer to override the globally used instance of {@link:http}.
|
||||
* All interactions with {@link:http} after the call to setHttp will use this new instance
|
||||
* (so make sure to call this method before dependant code using {@link:http} is ran and this
|
||||
* method is not called by any of your (indirect) dependencies.)
|
||||
* @param {HttpClient} newHttp the globally used instance of {@link:http}.
|
||||
*/
|
||||
export function setHttp(newHttp) {
|
||||
http = newHttp;
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { localize } from '@lion/localize';
|
||||
|
||||
/**
|
||||
* @typedef {import('./HttpClient').RequestTransformer} RequestTransformer
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} name the cookie name
|
||||
* @param {Document} _document overwriteable for testing
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export function getCookie(name, _document = document) {
|
||||
const match = _document.cookie.match(new RegExp(`(^|;\\s*)(${name})=([^;]*)`));
|
||||
return match ? decodeURIComponent(match[3]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a request, adding an accept-language header with the current application's locale
|
||||
* if it has not already been set.
|
||||
* @type {RequestTransformer}
|
||||
*/
|
||||
export function acceptLanguageRequestTransformer(request) {
|
||||
if (!request.headers.has('accept-language')) {
|
||||
request.headers.set('accept-language', localize.locale);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a request transformer that adds a XSRF header for protecting
|
||||
* against cross-site request forgery.
|
||||
* @param {string} cookieName the cookie name
|
||||
* @param {string} headerName the header name
|
||||
* @param {Document} _document overwriteable for testing
|
||||
* @returns {RequestTransformer}
|
||||
*/
|
||||
export function createXSRFRequestTransformer(cookieName, headerName, _document = document) {
|
||||
/**
|
||||
* @type {RequestTransformer}
|
||||
*/
|
||||
function xsrfRequestTransformer(request) {
|
||||
const xsrfToken = getCookie(cookieName, _document);
|
||||
if (xsrfToken) {
|
||||
request.headers.set(headerName, xsrfToken);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
return xsrfRequestTransformer;
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { expect } from '@open-wc/testing';
|
||||
import { http, setHttp } from '../src/http.js';
|
||||
import { HttpClient } from '../src/HttpClient.js';
|
||||
|
||||
describe('http', () => {
|
||||
it('exports an instance of HttpClient', () => {
|
||||
expect(http).to.be.an.instanceOf(HttpClient);
|
||||
});
|
||||
|
||||
it('can replace http with another instance', () => {
|
||||
const newHttp = new HttpClient();
|
||||
setHttp(newHttp);
|
||||
expect(http).to.equal(newHttp);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue