Skip to content

Commit

Permalink
Replace superagent with native fetch API. (#10342)
Browse files Browse the repository at this point in the history
* Replacing usage of superagent with fetch API.

* Removing superagent packages.

* Adding polyfills for IE11.

* Fixing linter hints.

* Supporting `window.fetch` in integration test environment.
  • Loading branch information
dennisoelkers committed Apr 12, 2021
1 parent 944b97a commit ce3579c
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 102 deletions.
7 changes: 4 additions & 3 deletions graylog2-web-interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"numeral": "^2.0.6",
"opensans-npm-webfont": "1.0.0",
"plotly.js": "^1.58.4",
"promise-polyfill": "^8.2.0",
"qs": "^6.3.0",
"react-ace": "9.2.1",
"react-color": "^2.14.0",
Expand All @@ -97,16 +98,15 @@
"react-ultimate-pagination": "^1.2.0",
"react-window": "^1.8.2",
"sockjs-client": "^1.4.0",
"superagent": "^6.1.0",
"superagent-bluebird-promise": "^4.1.0",
"terser-webpack-plugin": "^4.0.0",
"toastr": "^2.1.2",
"twix": "^1.1.4",
"typeahead.js": "^0.11.1",
"ua-parser-js": "^0.7.12",
"urijs": "^1.19.1",
"utility-types": "^3.10.0",
"uuid": "^3.2.1"
"uuid": "^3.2.1",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@babel/core": "7.13.10",
Expand Down Expand Up @@ -145,6 +145,7 @@
"json-loader": "^0.5.3",
"less": "^3.0.1",
"less-loader": "^7.0.0",
"node-fetch": "^2.6.1",
"puppeteer": "^2.1.1",
"react-styleguidist": "^11.0.8",
"style-loader": "2.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import request from 'superagent-bluebird-promise';
import BluebirdPromise from 'bluebird';

import FetchError from 'logic/errors/FetchError';
import ErrorsActions from 'actions/errors/ErrorsActions';
import StoreProvider from 'injection/StoreProvider';
Expand Down Expand Up @@ -58,11 +55,34 @@ const onServerError = (error, onUnauthorized = defaultOnUnauthorizedError) => {
throw fetchError;
};

const maybeStringify = (body: any) => (body && typeof body !== 'string' ? JSON.stringify(body) : body);

export class Builder {
private options = {};

private readonly url: string;

private readonly method: string;

private body: { body: any, mimeType: string };

private accept: string;

private responseHandler: (response: any) => any;

private errorHandler: (error: any) => any;

constructor(method, url) {
this.request = request(method, url.replace(/([^:])\/\//, '$1/'))
.set('X-Requested-With', 'XMLHttpRequest')
.set('X-Requested-By', 'XMLHttpRequest');
this.method = method;
this.url = url.replace(/([^:])\/\//, '$1/');

this.options = {
'X-Requested-With': 'XMLHttpRequest',
'X-Requested-By': 'XMLHttpRequest',
};

this.responseHandler = (response) => response;
this.errorHandler = undefined;
}

authenticated() {
Expand All @@ -73,90 +93,111 @@ export class Builder {
}

session(sessionId) {
this.request = this.request.auth(sessionId, 'session');
const buffer = Buffer.from(`${sessionId}:session`);

this.options = {
...this.options,
Authorization: `Basic ${buffer.toString('base64')}`,
};

return this;
}

setHeader(header, value) {
this.request = this.request.set(header, value);
this.options = {
...this.options,
[header]: value,
};

return this;
}

json(body) {
this.request = this.request
.send(body)
.type('json')
.accept('json')
.then((resp) => {
if (resp.ok) {
reportServerSuccess();
this.body = { body: maybeStringify(body), mimeType: 'application/json' };
this.accept = 'application/json';

this.responseHandler = (resp: Response) => {
if (resp.ok) {
reportServerSuccess();

return resp.json();
}

return resp.body;
}
throw new FetchError(resp.statusText, resp);
};

throw new FetchError(resp.statusText, resp);
}, (error) => onServerError(error));
this.errorHandler = (error) => onServerError(error);

return this;
}

file(body, mimeType) {
this.request = this.request
.send(body)
.type('json')
.accept(mimeType)
.parse(({ text }) => text)
.then((resp) => {
if (resp.ok) {
reportServerSuccess();
this.body = { body: maybeStringify(body), mimeType: 'application/json' };
this.accept = mimeType;

return resp.text;
}
this.responseHandler = (resp) => {
if (resp.ok) {
reportServerSuccess();

throw new FetchError(resp.statusText, resp);
}, (error) => onServerError(error));
return resp.text();
}

throw new FetchError(resp.statusText, resp);
};

this.errorHandler = (error) => onServerError(error);

return this;
}

plaintext(body) {
const onUnauthorized = () => history.replace(Routes.STARTPAGE);

this.request = this.request
.send(body)
.type('text/plain')
.accept('json')
.then((resp) => {
if (resp.ok) {
reportServerSuccess();
this.body = { body, mimeType: 'text/plain' };
this.accept = 'application/json';

this.responseHandler = (resp) => {
if (resp.ok) {
reportServerSuccess();

return resp.json();
}

return resp.body;
}
throw new FetchError(resp.statusText, resp);
};

throw new FetchError(resp.statusText, resp);
}, (error) => onServerError(error, onUnauthorized));
this.errorHandler = (error) => onServerError(error, onUnauthorized);

return this;
}

noSessionExtension() {
this.request = this.request.set('X-Graylog-No-Session-Extension', 'true');
this.options = {
...this.options,
'X-Graylog-No-Session-Extension': 'true',
};

return this;
}

build() {
return this.request;
const headers = this.body
? { ...this.options, 'Content-Type': this.body.mimeType }
: this.options;

return window.fetch(this.url, {
method: this.method,
headers,
body: this.body ? this.body.body : undefined,
}).then(this.responseHandler, this.errorHandler);
}
}

function queuePromiseIfNotLoggedin(promise) {
const SessionStore = StoreProvider.getStore('Session');

if (!SessionStore.isLoggedIn()) {
return () => new BluebirdPromise((resolve, reject) => {
return () => new Promise((resolve, reject) => {
const SessionActions = ActionsProvider.getActions('Session');

SessionActions.login.completed.listen(() => {
Expand All @@ -168,7 +209,7 @@ function queuePromiseIfNotLoggedin(promise) {
return promise;
}

export default function fetch(method, url, body) {
export default function fetch(method, url, body?) {
const promise = () => new Builder(method, url)
.authenticated()
.json(body)
Expand All @@ -186,7 +227,7 @@ export function fetchPlainText(method, url, body) {
return queuePromiseIfNotLoggedin(promise)();
}

export function fetchPeriodically(method, url, body) {
export function fetchPeriodically(method, url, body?) {
const promise = () => new Builder(method, url)
.authenticated()
.noSessionExtension()
Expand Down
4 changes: 4 additions & 0 deletions graylog2-web-interface/src/polyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@
*/
import 'core-js';
import 'regenerator-runtime/runtime';

// To support IE11 (remove if support is dropped)
import 'promise-polyfill/src/polyfill';
import 'whatwg-fetch';
4 changes: 3 additions & 1 deletion graylog2-web-interface/src/views/stores/SearchStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import asMock from 'helpers/mocking/AsMock';

import fetch from 'logic/rest/FetchProvider';
import Search from 'views/logic/search/Search';

Expand All @@ -23,7 +25,7 @@ jest.mock('logic/rest/FetchProvider', () => jest.fn());

describe('SearchStore', () => {
it('assigns a new search id when creating a search', () => {
fetch.mockImplementation((method, url, body) => Promise.resolve(body && JSON.parse(body)));
asMock(fetch).mockImplementation((method: string, url: string, body: any) => Promise.resolve(body && JSON.parse(body)));
const newSearch = Search.create();

return SearchActions.create(newSearch).then(({ search }) => {
Expand Down
12 changes: 12 additions & 0 deletions graylog2-web-interface/test/integration-environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class IntegrationEnvironment extends JSDomEnvironment {
version: '3.0.0',
tagline: 'Manage your logs in the dark and have lasers going and make it look like you\'re from space!',
}));

api.post(`${prefix}cluster/metrics/multiple`, (req, res) => res.json({}));
api.get(`${prefix}dashboards`, (req, res) => res.json([]));
api.get(`${prefix}search/decorators/available`, (req, res) => res.json({}));
Expand All @@ -47,28 +48,35 @@ class IntegrationEnvironment extends JSDomEnvironment {
api.get(`${prefix}system/sessions`, (req, res) => res.json({ session_id: null, username: null, is_valid: false }));
api.get(`${prefix}system/notifications`, (req, res) => res.json([]));
api.get(`${prefix}system/cluster/nodes`, (req, res) => res.json({ nodes: [], total: 0 }));

api.get(`${prefix}system/cluster_config/org.graylog2.indexer.searches.SearchesClusterConfig`, (req, res) => res.json({
relative_timerange_options: {},
query_time_range_limit: 'P5M',
}));

api.get(`${prefix}views`, (req, res) => res.json({
total: 0,
page: 1,
per_page: 1,
count: 0,
views: [],
}));

api.get(`${prefix}views/fields`, (req, res) => res.json([]));
api.get(`${prefix}views/functions`, (req, res) => res.json([]));

api.post(`${prefix}views/search`, (req, res) => {
const search = req.body;
searches[search.id] = search;

return res.json(search);
});

api.post(`${prefix}views/search/metadata`, (req, res) => res.json({
query_metadata: {},
declared_parameters: {},
}));

api.post(/views\/search\/(\w+)\/execute$/, (req, res) => {
const search = searches[req.params[0]];
const results = search.queries.map(({ id }) => [id, {
Expand All @@ -84,6 +92,7 @@ class IntegrationEnvironment extends JSDomEnvironment {
state: 'COMPLETED',
}])
.reduce((prev, [id, result]) => ({ ...prev, [id]: result }), {});

return res.json({
id: `${req.params[0]}-job`,
search_id: req.params[0],
Expand All @@ -101,6 +110,9 @@ class IntegrationEnvironment extends JSDomEnvironment {
const { port } = this.server.address();

this.global.api_url = `http://localhost:${port}${prefix}`;

// eslint-disable-next-line global-require
this.global.window.fetch = require('node-fetch');
}

async setup() {
Expand Down

0 comments on commit ce3579c

Please sign in to comment.