Skip to content
This repository has been archived by the owner on Aug 15, 2023. It is now read-only.

Commit

Permalink
refactor(shared): combine BackendError with APIError
Browse files Browse the repository at this point in the history
  • Loading branch information
ascariandrea committed Nov 7, 2022
1 parent 6f85623 commit 5bac6b9
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 89 deletions.
45 changes: 0 additions & 45 deletions packages/shared/src/backend/errors/BackendError.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NotAuthorizedError } from '../errors/BackendError';
import { NotAuthorizedError } from '../../errors/APIError';
import * as express from 'express';
import * as E from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';
Expand Down
12 changes: 4 additions & 8 deletions packages/shared/src/backend/utils/routeHandlerMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express from 'express';
import _ from 'lodash';
import { APIError } from '../../errors/APIError';
import { isAPIError } from '../../errors/APIError';
import { GetLogger } from '../../logger';

const logger = GetLogger('route-handler');
Expand Down Expand Up @@ -93,10 +93,7 @@ export const routeHandleMiddleware = <
} catch (error) {
logger.error('Route handler (%s) error: %O', fname, error);

// todo: this should be 500
res.status(502);

if (error instanceof APIError) {
if (isAPIError(error)) {
logger.error(
'APIError - %s: (%s) %s %s',
error.name,
Expand All @@ -105,20 +102,19 @@ export const routeHandleMiddleware = <
);
res.status(error.status);
res.send({
name: error.type,
name: error.name,
message: error.message,
details: error.details,
});
} else if (error instanceof Error) {
logger.error('Error - %s: %s', error.name, error.message);

res.send('Software error: ' + error.message);
logger.error(
'Error in HTTP handler API(%s): %s %s',
fname,
error.message,
error.stack
);
res.status(500).send('Software error: ' + error.message);
} else {
res.status(502);
res.send('Software error: ' + (error as any).message);
Expand Down
10 changes: 6 additions & 4 deletions packages/shared/src/components/Error/ErrorBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import { isAPIError } from '../../errors/APIError';
export const ErrorBox = (e: unknown): React.ReactElement<any, string> => {
// eslint-disable-next-line
console.log('Displaying error', e);
const errorName = isAPIError(e) ? e.name : 'Error';
const message = isAPIError(e) ? e.message : 'Unknown Error';
return (
<Card>
<Alert severity="error">
<AlertTitle>{e instanceof Error ? e.name : 'Error'}</AlertTitle>
<p>{e instanceof Error ? e.message : 'Unknown error'}</p>
{isAPIError(e) ? (
<AlertTitle>{errorName}</AlertTitle>
<p>{message}</p>
{isAPIError(e) && e.details.kind === 'DecodingError' ? (
<ul>
{(e.details ?? []).map((d) => (
{(e.details.errors ?? []).map((d: any) => (
<li key={d}>{d}</li>
))}
</ul>
Expand Down
5 changes: 4 additions & 1 deletion packages/shared/src/endpoints/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ const decodeOrThrowRequest = <E extends MinimalEndpointInstance>(
): DecodeRequestSuccess<E>['result'] => {
return pipe(decodeRequest(e, r), (r) => {
if (r.type === 'error') {
throw new APIError(400, 'Bad Request', 'Request decode failed', r.result);
throw new APIError('Bad Request', {
kind: 'DecodingError',
errors: r.result,
});
}

return r.result;
Expand Down
75 changes: 66 additions & 9 deletions packages/shared/src/errors/APIError.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,73 @@
import { AxiosError } from 'axios';
import { IOError } from 'ts-io-error/lib';

export const isAPIError = (e: unknown): e is APIError => {
return (e as any).name === 'APIError';
};

export class APIError extends Error {
export class APIError extends IOError {
public readonly name = 'APIError';
}

export const NotAuthorizedError = (): APIError => {
return {
name: 'APIError',
status: 401,
message: 'Authorization header is missing',
details: {
kind: 'ClientError',
status: '401',
},
};
};

constructor(
public readonly status: number,
public readonly type: string,
public readonly message: string,
public readonly details: string[]
) {
super(message);
export const NotFoundError = (resource: string): APIError => {
return {
name: 'APIError',
status: 404,
message: `Can't find the resource ${resource}`,
details: {
kind: 'ClientError',
status: '404',
},
};
};

export const fromAxiosError = (e: AxiosError): APIError => {
return (
e.response?.data ?? {
name: 'APIError',
status: e.response?.status ?? 500,
message: e.message,
details: {
kind: 'ServerError',
status: '500',
meta: e.response?.data,
},
}
);
};

export const toAPIError = (e: unknown): APIError => {
// eslint-disable-next-line
console.error(e);
if (e instanceof APIError) {
return new APIError(e.message, {
kind: 'ServerError',
status: e.status + '',
meta: e.details,
});
}
}

if (e instanceof Error) {
return new APIError(e.message, {
kind: 'ServerError',
status: e.name,
});
}

return new APIError('UnknownError', {
kind: 'ServerError',
status: 'Unknown',
});
};
6 changes: 5 additions & 1 deletion packages/shared/src/errors/AppError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ export class AppError {

export const toAppError = (e: unknown): AppError => {
if (e instanceof APIError) {
return e;
return new AppError(
e.name,
e.message,
e.details.kind === 'DecodingError' ? (e.details.errors as any[]) : []
);
}

if (e instanceof Error) {
Expand Down
16 changes: 13 additions & 3 deletions packages/shared/src/errors/ValidationError.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { failure } from 'io-ts/lib/PathReporter';
import { APIError, isAPIError } from './APIError';
import * as t from 'io-ts';

export const toValidationError = (
message: string,
details: string[]
errors: t.ValidationError[]
): APIError => {
return new APIError(400, 'ValidationError', message, details);
return {
name: 'APIError',
message,
status: 400,
details: {
kind: 'DecodingError',
errors: failure(errors),
},
};
};

export const isValidationError = (
e: unknown
): e is APIError & { type: 'ValidationError' } => {
return isAPIError(e) && e.type === 'ValidationError';
return isAPIError(e) && e.details.kind === 'DecodingError';
};
46 changes: 29 additions & 17 deletions packages/shared/src/providers/api.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import * as TE from 'fp-ts/lib/TaskEither';
import * as t from 'io-ts';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { MinimalEndpointInstance, TypeOfEndpointInstance } from '../endpoints';
import { APIError } from '../errors/APIError';
import { APIError, fromAxiosError, isAPIError } from '../errors/APIError';
import { toValidationError } from '../errors/ValidationError';
import { trexLogger } from '../logger';

Expand All @@ -24,21 +24,37 @@ export const apiLogger = trexLogger.extend('API');
export const toAPIError = (e: unknown): APIError => {
// eslint-disable-next-line
apiLogger.error('An error occurred %O', e);
if (isAPIError(e)) {
return e;
}

if (axios.isAxiosError(e)) {
return fromAxiosError(e);
}

if (e instanceof Error) {
if (e.message === 'Network Error') {
return new APIError(
502,
'Network Error',
'The API endpoint is not reachable',
["Be sure you're connected to internet."]
);
return new APIError('Network Error', {
kind: 'NetworkError',
status: '500',
meta: [
'The API endpoint is not reachable',
"Be sure you're connected to internet.",
],
});
}
return new APIError(500, 'UnknownError', e.message, []);
return new APIError(e.message, {
kind: 'ClientError',
meta: e.stack,
status: '500',
});
}

return new APIError(500, 'UnknownError', 'An error occurred', [
JSON.stringify(e),
]);
return new APIError('An error occurred', {
kind: 'ClientError',
meta: JSON.stringify(e),
status: '500',
});
};

const liftFetch = <B>(
Expand All @@ -51,11 +67,7 @@ const liftFetch = <B>(
TE.chain((content) => {
return pipe(
decode(content),
E.mapLeft((e): APIError => {
const details = PathReporter.report(E.left(e));
apiLogger.error('toAPIError Validation failed %O', details);
return toValidationError('Validation failed', details);
}),
E.mapLeft((e) => toValidationError('Validation failed', e)),
TE.fromEither
);
})
Expand Down Expand Up @@ -128,7 +140,7 @@ export const MakeHTTPClient = (client: AxiosInstance): HTTPClient => {
TE.mapLeft((e): APIError => {
const details = PathReporter.report(E.left(e));
apiLogger.error('MakeHTTPClient Validation failed %O', details);
return toValidationError('Validation failed', details);
return toValidationError('Validation failed', e);
}),
TE.chain((input) => {
const url = e.getPath(input.params);
Expand Down

0 comments on commit 5bac6b9

Please sign in to comment.