Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancelling request in onRequest handlers #23

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions src/service.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { reduceHandlers } from './utils';

/**
* @property {Array} middlewares stack
* @property {AxiosInstance} http
Expand Down Expand Up @@ -101,10 +103,22 @@ export default class HttpMiddlewareService {
* @returns {Promise}
*/
adapter(config) {
return this.chain.reduce(
(acc, [onResolve, onError]) => acc.then(onResolve, onError),
Promise.resolve(config)
);
const { request: requestHandlers, response } = this.chain;
return reduceHandlers(requestHandlers, Promise.resolve(config))
.then(
(conf) => {
if (!conf) {
const err = Error('Request cancelled within a middleware');
err.type = 'REQUEST_CANCELLED';
return Promise.reject(err);
}
return reduceHandlers(
response,
this._onSync(this.originalAdapter.call(this.http, conf))
);
},
err => reduceHandlers(response, Promise.reject(err))
);
}

/**
Expand All @@ -113,12 +127,17 @@ export default class HttpMiddlewareService {
* @private
*/
_addMiddleware(middleware) {
this.chain.unshift([
middleware.onRequest && (conf => middleware.onRequest(conf)),
if (!this.chain) {
this._updateChain();
return;
}

this.chain.request.unshift([
middleware.onRequest && (conf => conf && middleware.onRequest(conf)),
middleware.onRequestError && (error => middleware.onRequestError(error)),
]);

this.chain.push([
this.chain.response.push([
middleware.onResponse && (response => middleware.onResponse(response)),
middleware.onResponseError && (error => middleware.onResponseError(error)),
]);
Expand All @@ -128,7 +147,10 @@ export default class HttpMiddlewareService {
* @private
*/
_updateChain() {
this.chain = [[conf => this._onSync(this.originalAdapter.call(this.http, conf)), undefined]];
this.chain = {
request: [],
response: [],
};
this.middlewares.forEach(middleware => this._addMiddleware(middleware));
}

Expand Down
4 changes: 4 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const reduceHandlers = (handlers, promise) => handlers.reduce(
(acc, [onResolve, onError]) => acc.then(onResolve, onError),
promise
);
15 changes: 9 additions & 6 deletions test/mocks/MiddlewareMock.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
const identity = a => a;
const reject = err => Promise.reject(err);

export default class MiddlewareMock {
constructor() {
constructor(id = 'MiddlewareMock') {
Object.assign(this, {
onRequest: jest.fn(config => config),
onRequestError: jest.fn(),
onSync: jest.fn(promise => promise),
onResponse: jest.fn(response => response),
onResponseError: jest.fn(),
onRequest: jest.fn(identity).mockName(`${id}.onRequest`),
onRequestError: jest.fn(reject).mockName(`${id}.onRequestError`),
onSync: jest.fn(identity).mockName(`${id}.onSync`),
onResponse: jest.fn(response => response).mockName(`${id}.onResponse`),
onResponseError: jest.fn(reject).mockName(`${id}.onResponseError`),
});
}
}
68 changes: 68 additions & 0 deletions test/specs/cancellation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Service } from '../../dist/axios-middleware.common';
import MiddlewareMock from '../mocks/MiddlewareMock';

describe('onRequest cancellation', () => {
it('can reject a request', () => {
const service = new Service();
service.originalAdapter = jest.fn(conf => Promise.resolve(conf));

const middleware1 = new MiddlewareMock('middleware1');
const middleware2 = new MiddlewareMock('middleware2');

middleware1.onRequest.mockImplementation(() => Promise.reject());

service.register([
middleware2,
middleware1,
]);

return service.adapter('test').catch(() => {
expect(middleware1.onRequest).toBeCalledTimes(1);
expect(middleware2.onRequest).not.toBeCalled();

expect(middleware1.onRequestError).not.toBeCalled();
expect(middleware2.onRequestError).toBeCalledTimes(1);

expect(middleware1.onSync).not.toBeCalled();
expect(middleware2.onSync).not.toBeCalled();

expect(middleware2.onResponse).not.toBeCalled();
expect(middleware1.onResponse).not.toBeCalled();

expect(middleware2.onResponseError).toBeCalledTimes(1);
expect(middleware1.onResponseError).toBeCalledTimes(1);
});
});

it('can cancel a request by returning false', () => {
const service = new Service();
service.originalAdapter = jest.fn(conf => Promise.resolve(conf));

const middleware1 = new MiddlewareMock(1);
const middleware2 = new MiddlewareMock(2);

middleware1.onRequest.mockImplementation(() => false);

service.register([
middleware2,
middleware1,
]);

return service.adapter('test').catch(() => {
expect(middleware1.onRequest).toBeCalledTimes(1);
expect(middleware2.onRequest).not.toBeCalled();

expect(middleware1.onRequestError).not.toBeCalled();
expect(middleware2.onRequestError).not.toBeCalled();

expect(middleware1.onSync).not.toBeCalled();
expect(middleware2.onSync).not.toBeCalled();

expect(middleware2.onResponse).not.toBeCalled();
expect(middleware1.onResponse).not.toBeCalled();

expect(middleware2.onResponseError).not.toBeCalled();
expect(middleware1.onResponseError).not.toBeCalled();
});
});
});
27 changes: 0 additions & 27 deletions test/specs/service.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { Service } from '../../dist/axios-middleware.common';
import MiddlewareMock from '../mocks/MiddlewareMock';

const http = axios.create();
const mock = new MockAdapter(http);
Expand All @@ -18,32 +17,6 @@ describe('Middleware service', () => {
mock.restore();
});

it('throws when adding the same middleware instance', () => {
const middleware = {};

service.register(middleware);

expect(() => service.register(middleware)).toThrow();
});

it('works with both middleware syntaxes', () => {
expect.assertions(2);
const middleware = new MiddlewareMock();
const simplifiedSyntax = {
onRequest: jest.fn(config => config),
};

service.register([
middleware,
simplifiedSyntax,
]);

service.adapter().then(() => {
expect(middleware.onRequest).toHaveBeenCalled();
expect(simplifiedSyntax.onRequest).toHaveBeenCalled();
});
});

it('runs the middlewares in order', () => {
expect.assertions(1);

Expand Down
34 changes: 34 additions & 0 deletions test/specs/syntax.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Service } from '../../dist/axios-middleware.common';
import MiddlewareMock from '../mocks/MiddlewareMock';

describe('Middleware syntax', () => {
it('throws when adding the same middleware instance', () => {
const service = new Service();
const middleware = {};

service.register(middleware);

expect(() => service.register(middleware)).toThrow();
});

it('works with both middleware syntaxes', () => {
expect.assertions(2);
const service = new Service();
service.originalAdapter = jest.fn(conf => Promise.resolve(conf));

const middleware = new MiddlewareMock();
const simplifiedSyntax = {
onRequest: jest.fn(config => config),
};

service.register([
middleware,
simplifiedSyntax,
]);

return service.adapter({}).then(() => {
expect(middleware.onRequest).toHaveBeenCalled();
expect(simplifiedSyntax.onRequest).toHaveBeenCalled();
});
});
});