Skip to content

Commit

Permalink
refactor(bus): humanize axios error messages (#89)
Browse files Browse the repository at this point in the history
closes #88
  • Loading branch information
derevnjuk authored May 16, 2022
1 parent dc5536c commit 80b7785
Show file tree
Hide file tree
Showing 15 changed files with 129 additions and 63 deletions.
17 changes: 17 additions & 0 deletions packages/bus/src/dispatchers/HttpCommandDispatcher.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HttpRequest } from '../commands';
import { HttpCommandDispatcher } from './HttpCommandDispatcher';
import { HttpCommandDispatcherConfig } from './HttpCommandDispatcherConfig';
import { HttpCommandError } from '../exceptions';
import { RetryStrategy } from '@sec-tester/core';
import {
anyFunction,
Expand Down Expand Up @@ -225,5 +226,21 @@ describe('HttpCommandDispatcher', () => {
// assert
verify(mockedRetryStrategy.acquire(anyFunction())).once();
});

it('should throw an instance of HttpCommandError', async () => {
// arrange
const command = new HttpRequest({
url: '/api/test',
payload: undefined,
method: 'GET',
expectReply: true
});
nock(baseUrl).get('/api/test').replyWithError('Something went wrong.');

// act & assert
await expect(axiosDispatcher.execute(command)).rejects.toThrow(
HttpCommandError
);
});
});
});
60 changes: 30 additions & 30 deletions packages/bus/src/dispatchers/HttpCommandDispatcher.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { HttpCommandDispatcherConfig } from './HttpCommandDispatcherConfig';
import { HttpRequest } from '../commands';
import { HttpCommandError } from '../exceptions';
import { CommandDispatcher, RetryStrategy } from '@sec-tester/core';
import { inject, injectable } from 'tsyringe';
import axios, { AxiosRequestConfig } from 'axios';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import rateLimit, { RateLimitedAxiosInstance } from 'axios-rate-limit';
import FormData from 'form-data';
import { finished, Readable } from 'stream';
Expand All @@ -24,9 +25,8 @@ export class HttpCommandDispatcher implements CommandDispatcher {
public async execute<T, R>(
command: HttpRequest<T, R>
): Promise<R | undefined> {
const requestOptions = this.convertToHttpOptions(command);
const response = await this.retryStrategy.acquire(() =>
this.client.request(requestOptions)
this.performHttpRequest(command)
);

if (!command.expectReply && response.data instanceof Readable) {
Expand All @@ -38,33 +38,33 @@ export class HttpCommandDispatcher implements CommandDispatcher {
}
}

private convertToHttpOptions<T, R>(
command: HttpRequest<T, R>
): AxiosRequestConfig<T> {
const {
url,
params,
method,
expectReply,
correlationId,
createdAt,
payload: data,
ttl: timeout
} = command;

return {
url,
method,
data,
timeout,
params,
headers: {
...this.inferHeaders(data),
'x-correlation-id': correlationId,
'date': createdAt.toISOString()
},
...(!expectReply ? { responseType: 'stream' } : {})
};
private async performHttpRequest<T, R>({
correlationId,
createdAt,
expectReply,
method,
params,
payload,
ttl,
url
}: HttpRequest<T, R>): Promise<AxiosResponse<R>> {
try {
return await this.client.request<R, AxiosResponse<R>, T>({
url,
method,
params,
data: payload,
timeout: ttl,
headers: {
...this.inferHeaders(payload),
'x-correlation-id': correlationId,
'date': createdAt.toISOString()
},
...(!expectReply ? { responseType: 'stream' } : {})
});
} catch (e) {
throw new HttpCommandError(e);
}
}

private createHttpClient(): RateLimitedAxiosInstance {
Expand Down
30 changes: 30 additions & 0 deletions packages/bus/src/exceptions/HttpCommandError.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { HttpCommandError } from './HttpCommandError';
import { AxiosError } from 'axios';

describe('HttpCommandError', () => {
describe('constructor', () => {
it.each([
{
input: { message: 'Something went wrong' },
expected: { message: 'Something went wrong' }
},
{
input: { response: { data: 'Something went wrong', status: 500 } },
expected: { message: 'Something went wrong', status: 500 }
},
{
input: { message: 'Timeout reached', code: 'ETIMEDOUT' },
expected: { message: 'Timeout reached', code: 'ETIMEDOUT' }
}
])(
'should create an instance of error if $input is passed',
({ input, expected }) => {
// act
const result = new HttpCommandError(input as AxiosError);

// assert
expect(result).toMatchObject(expected);
}
);
});
});
13 changes: 13 additions & 0 deletions packages/bus/src/exceptions/HttpCommandError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { AxiosError } from 'axios';
import { SecTesterError } from '@sec-tester/core';

export class HttpCommandError extends SecTesterError {
public readonly status: number | undefined;
public readonly code: string | undefined;

constructor(public readonly cause: AxiosError) {
super(cause.response?.data ?? cause.message);
this.status = cause.response?.status;
this.code = cause.code;
}
}
1 change: 1 addition & 0 deletions packages/bus/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './HttpCommandError';
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import { ExponentialBackoffRetryStrategy } from './ExponentialBackoffRetryStrate
class TestError extends Error {
constructor(
options:
| { code: number | string }
| { isAxiosError?: boolean; response?: { status: number } }
| { code: number | string | undefined }
| { status: number | undefined } = { status: undefined }
) {
super('Something went wrong.');
Object.assign(this, options);
this.name = new.target.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}

Expand Down Expand Up @@ -98,8 +97,7 @@ describe('ExponentialBackoffRetryStrategy', () => {
.map(
(_: unknown, idx: number) =>
new TestError({
isAxiosError: true,
response: { status: 500 + idx }
status: 500 + idx
})
)
])('should retry on the error (%j)', async (error: Error) => {
Expand Down Expand Up @@ -134,7 +132,7 @@ describe('ExponentialBackoffRetryStrategy', () => {
const retryStrategy = new ExponentialBackoffRetryStrategy({
maxDepth: 2
});
const error = new TestError({ isAxiosError: true });
const error = new TestError();
const input = jest.fn().mockRejectedValueOnce(error);

const result = retryStrategy.acquire(input);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HttpCommandError } from '../exceptions';
import { RetryStrategy, delay } from '@sec-tester/core';
import { AxiosError } from 'axios';
import ErrnoException = NodeJS.ErrnoException;

export interface ExponentialBackoffOptions {
Expand Down Expand Up @@ -53,16 +53,8 @@ export class ExponentialBackoffRetryStrategy implements RetryStrategy {
);
}

const axiosError = (err as AxiosError).isAxiosError
? (err as AxiosError)
: undefined;
const status = (err as HttpCommandError).status ?? 200;

if (axiosError) {
const httpStatus = axiosError.response?.status ?? 200;

return httpStatus >= 500;
}

return false;
return status >= 500;
}
}
6 changes: 3 additions & 3 deletions packages/core/src/bus/exceptions/EventHandlerNotFound.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export class EventHandlerNotFound extends Error {
import { SecTesterError } from '../../exceptions';

export class EventHandlerNotFound extends SecTesterError {
constructor(...eventNames: string[]) {
super(
`Event handler not found. Please register a handler for the following events: ${eventNames.join(
', '
)}`
);
this.name = new.target.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}
10 changes: 6 additions & 4 deletions packages/core/src/bus/exceptions/IllegalOperation.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { CommandDispatcher } from '../CommandDispatcher';
import { EventDispatcher } from '../EventDispatcher';
import { SecTesterError } from '../../exceptions';
import { getTypeName } from '../../utils';

export class IllegalOperation extends Error {
export class IllegalOperation extends SecTesterError {
constructor(instance: EventDispatcher | CommandDispatcher) {
super(
`Please make sure that ${instance.constructor.name} established a connection with host.`
`Please make sure that ${getTypeName(
instance
)} established a connection with host.`
);
this.name = new.target.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}
6 changes: 3 additions & 3 deletions packages/core/src/bus/exceptions/NoResponse.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export class NoResponse extends Error {
import { SecTesterError } from '../../exceptions';

export class NoResponse extends SecTesterError {
constructor(duration: number) {
super(`No response for ${duration} seconds.`);
this.name = new.target.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}
10 changes: 6 additions & 4 deletions packages/core/src/bus/exceptions/NoSubscriptionsFound.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { EventHandler } from '../EventHandler';
import { SecTesterError } from '../../exceptions';
import { getTypeName } from '../../utils';

export class NoSubscriptionsFound extends Error {
export class NoSubscriptionsFound extends SecTesterError {
constructor(handler: EventHandler<unknown, unknown>) {
super(
`No subscriptions found. Please use '@bind()' decorator to subscribe ${handler.constructor.name} to events.`
`No subscriptions found. Please use '@bind()' decorator to subscribe ${getTypeName(
handler
)} to events.`
);
this.name = new.target.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}
7 changes: 5 additions & 2 deletions packages/core/src/bus/exceptions/UnsupportedEventType.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export class UnsupportedEventType extends Error {
import { SecTesterError } from '../../exceptions';
import { getTypeName } from '../../utils';

export class UnsupportedEventType extends SecTesterError {
constructor(event: unknown) {
super(`${typeof event} cannot be used with the @bind decorator.`);
super(`${getTypeName(event)} cannot be used with the @bind decorator.`);
}
}
6 changes: 6 additions & 0 deletions packages/core/src/exceptions/SecTesterError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class SecTesterError extends Error {
constructor(message: string) {
super(message);
this.name = new.target.name;
}
}
1 change: 1 addition & 0 deletions packages/core/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './SecTesterError';
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import './register';
export * from './bus';
export * from './configuration';
export * from './credentials-provider';
export * from './exceptions';
export * from './logger';
export {
delay,
Expand Down

0 comments on commit 80b7785

Please sign in to comment.