diff --git a/packages/shared/src/backend/errors/BackendError.ts b/packages/shared/src/backend/errors/BackendError.ts deleted file mode 100644 index ac6f8aaf7..000000000 --- a/packages/shared/src/backend/errors/BackendError.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { IOError } from 'ts-io-error/lib'; - -export class BackendError extends IOError { - public readonly name = 'BackendError'; -} - -export const NotAuthorizedError = (): BackendError => { - return { - name: 'BackendError', - status: 401, - message: 'Authorization header is missing', - details: { - kind: 'ClientError', - status: '401', - }, - }; -}; - -export const NotFoundError = (resource: string): BackendError => { - return { - name: 'BackendError', - status: 404, - message: `Can't find the resource ${resource}`, - details: { - kind: 'ClientError', - status: '404', - }, - }; -}; - -export const toBackendError = (e: unknown): BackendError => { - // eslint-disable-next-line - console.error(e); - if (e instanceof Error) { - return new BackendError(e.message, { - kind: 'ServerError', - status: e.name, - }); - } - - return new BackendError('UnknownError', { - kind: 'ServerError', - status: 'Unknown', - }); -}; diff --git a/packages/shared/src/backend/utils/authenticationMiddleware.ts b/packages/shared/src/backend/utils/authenticationMiddleware.ts index db3408e36..690e00157 100644 --- a/packages/shared/src/backend/utils/authenticationMiddleware.ts +++ b/packages/shared/src/backend/utils/authenticationMiddleware.ts @@ -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'; diff --git a/packages/shared/src/backend/utils/routeHandlerMiddleware.ts b/packages/shared/src/backend/utils/routeHandlerMiddleware.ts index a82b4031b..da6facbd2 100644 --- a/packages/shared/src/backend/utils/routeHandlerMiddleware.ts +++ b/packages/shared/src/backend/utils/routeHandlerMiddleware.ts @@ -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'); @@ -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, @@ -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); diff --git a/packages/shared/src/components/Error/ErrorBox.tsx b/packages/shared/src/components/Error/ErrorBox.tsx index 5b7dfca85..a2c5685f9 100644 --- a/packages/shared/src/components/Error/ErrorBox.tsx +++ b/packages/shared/src/components/Error/ErrorBox.tsx @@ -7,14 +7,16 @@ import { isAPIError } from '../../errors/APIError'; export const ErrorBox = (e: unknown): React.ReactElement => { // 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 ( - {e instanceof Error ? e.name : 'Error'} -

{e instanceof Error ? e.message : 'Unknown error'}

- {isAPIError(e) ? ( + {errorName} +

{message}

+ {isAPIError(e) && e.details.kind === 'DecodingError' ? (
    - {(e.details ?? []).map((d) => ( + {(e.details.errors ?? []).map((d: any) => (
  • {d}
  • ))}
diff --git a/packages/shared/src/endpoints/helper.ts b/packages/shared/src/endpoints/helper.ts index 53088ea24..02d7b29a4 100644 --- a/packages/shared/src/endpoints/helper.ts +++ b/packages/shared/src/endpoints/helper.ts @@ -82,7 +82,10 @@ const decodeOrThrowRequest = ( ): DecodeRequestSuccess['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; diff --git a/packages/shared/src/errors/APIError.ts b/packages/shared/src/errors/APIError.ts index f79176489..ecaa5cdb6 100644 --- a/packages/shared/src/errors/APIError.ts +++ b/packages/shared/src/errors/APIError.ts @@ -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', + }); +}; diff --git a/packages/shared/src/errors/AppError.ts b/packages/shared/src/errors/AppError.ts index e6064d75d..2f4d9c4fa 100644 --- a/packages/shared/src/errors/AppError.ts +++ b/packages/shared/src/errors/AppError.ts @@ -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) { diff --git a/packages/shared/src/errors/ValidationError.ts b/packages/shared/src/errors/ValidationError.ts index e996b7833..9e9c81c20 100644 --- a/packages/shared/src/errors/ValidationError.ts +++ b/packages/shared/src/errors/ValidationError.ts @@ -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'; }; diff --git a/packages/shared/src/providers/api.provider.ts b/packages/shared/src/providers/api.provider.ts index c96d1e5e5..e8bf16d22 100644 --- a/packages/shared/src/providers/api.provider.ts +++ b/packages/shared/src/providers/api.provider.ts @@ -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'; @@ -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 = ( @@ -51,11 +67,7 @@ const liftFetch = ( 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 ); }) @@ -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);