feature - error catcher middleware
MODIFY
example/index.ts
example/index.ts
@@ -16,7 +16,7 @@ const createServer = () => {
hello$,
];
- const app = httpListener(middlewares, effects);
+ const app = httpListener({ middlewares, effects });
const httpServer = http
.createServer(app)

MODIFY
src/effects/effects.combiner.ts
src/effects/effects.combiner.ts
import { merge, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
-import { EffectCombiner, MiddlewareCombiner } from './effects.interface';
import { HttpRequest } from '../http.interface';
+import { EffectCombiner, MiddlewareCombiner } from './effects.interface';
export const combineEffects: EffectCombiner = effects => res => req => {
const req$ = of(req);
- const mappedEffects = effects.map(effect => effect(req$, res));
+ const mappedEffects = effects.map(effect => effect(req$, res, undefined));
return merge(...mappedEffects);
};
export const combineMiddlewareEffects: MiddlewareCombiner = effects => res => req => {
const req$ = of(req);
const mappedEffects = effects.map(effect =>
- switchMap((mappedReqest: HttpRequest) => effect(of(mappedReqest), res))
+ switchMap((mappedReqest: HttpRequest) => effect(of(mappedReqest), res, undefined))
);
return req$.pipe(...mappedEffects);
};

MODIFY
src/effects/effects.interface.ts
src/effects/effects.interface.ts
@@ -8,9 +8,10 @@ export type EffectResponse = {
headers?: Record<string, string>,
};
-export type Effect<T = EffectResponse> = (
+export type Effect<T = EffectResponse, U = any> = (
request$: Observable<HttpRequest>,
response: HttpResponse,
+ metadata: U,
) => Observable<T>;

MODIFY
src/http.listener.ts
src/http.listener.ts
-import { Subject } from 'rxjs';
-import { defaultIfEmpty, tap, switchMap } from 'rxjs/operators';
+import { Subject, of } from 'rxjs';
+import { catchError, defaultIfEmpty, switchMap, tap } from 'rxjs/operators';
+import { Effect, combineEffects, combineMiddlewareEffects } from './effects';
import { Http, HttpRequest, HttpResponse } from './http.interface';
-import { combineEffects, Effect, combineMiddlewareEffects } from './effects';
+import { errorCatcher$ } from './middlewares/errorCatcher.middleware';
import { handleResponse } from './response';
import { StatusCode } from './util';
-export const httpListener = (middlewares: Effect<HttpRequest>[], effects: Effect[]) => {
+type HttpListenerConfig = {
+ middlewares: Effect<HttpRequest>[],
+ effects: Effect[],
+};
+
+export const httpListener = ({ middlewares, effects }: HttpListenerConfig) => {
const request$ = new Subject<Http>();
const effect$ = request$
.pipe(
@@ -14,7 +20,8 @@ export const httpListener = (middlewares: Effect<HttpRequest>[], effects: Effect
.pipe(
switchMap(combineEffects(effects)(res)),
defaultIfEmpty({ status: StatusCode.NOT_FOUND }),
- tap(handleResponse(res))
+ tap(handleResponse(res)),
+ catchError(error => errorCatcher$(of(req), res, error))
)
)
);

MODIFY
src/middlewares/bodyParser.middleware.ts
src/middlewares/bodyParser.middleware.ts
-import { Observable } from 'rxjs';
-import { switchMap, tap, toArray, map } from 'rxjs/operators';
+import { Observable, throwError } from 'rxjs';
+import { catchError, map, switchMap, tap, toArray } from 'rxjs/operators';
+import { Effect } from '../effects';
import { HttpRequest } from '../http.interface';
-import { Effect } from '../effects/effects.interface';
import { ContentType } from '../util';
+import { ServerError } from '../util/error.util';
export const fromReadableStream = (stream: HttpRequest): Observable<any> => {
stream.pause();
@@ -41,14 +42,15 @@ export const getBody = (req: HttpRequest) =>
}),
);
-export const bodyParser$: Effect<HttpRequest> = request$ => request$
+export const bodyParser$: Effect<HttpRequest> = (request$, response) => request$
.pipe(
switchMap(req =>
req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH'
? request$.pipe(
switchMap(getBody),
tap(body => req.body = body),
- map(body => req)
+ map(body => req),
+ catchError(error => throwError(new ServerError('Request body parse error', error)))
)
: request$
)

ADD
src/middlewares/errorCatcher.middleware.ts
+import { tap } from 'rxjs/operators';
+import { Effect } from '../effects';
+import { HttpRequest } from '../http.interface';
+import { DEFAULT_HEADERS } from '../response';
+import { StatusCode } from '../util';
+import { HttpError, ServerError, isHttpError, isServerError } from '../util/error.util';
+
+export type Error = HttpError | ServerError;
+
+export const getStatusCode = (error: Error): StatusCode => {
+ if (isServerError(error)) {
+ return StatusCode.BAD_REQUEST;
+ }
+
+ if (isHttpError(error)) {
+ return error.status;
+ }
+
+ return StatusCode.INTERNAL_SERVER_ERROR;
+};
+
+export const errorCatcher$: Effect<HttpRequest, Error> = (request$, response, error) => request$
+ .pipe(
+ tap(req => {
+ const status = getStatusCode(error);
+ const errorObject = {
+ error: {
+ status,
+ message: error.message,
+ },
+ };
+
+ response.writeHead(status, DEFAULT_HEADERS);
+ response.end(JSON.stringify(errorObject));
+ })
+ );

RENAME
src/util/content-type.util.ts
src/util/contentType.util.ts
ADD
src/util/error.util.ts
+import { StatusCode } from './statusCode.util';
+
+export class ExtendableError extends Error {
+ constructor(
+ public name: string,
+ message: string
+ ) {
+ super(message);
+ }
+}
+
+export class HttpError extends ExtendableError {
+ constructor(
+ public readonly message: string,
+ public readonly status: StatusCode,
+ ) {
+ super('HttpError', message);
+ }
+}
+
+export class ServerError extends ExtendableError {
+ constructor(
+ public readonly message: string,
+ public readonly error: any,
+ ) {
+ super('ServerError', message);
+ }
+}
+
+export const isHttpError = (error: Error): error is HttpError =>
+ error.name === 'HttpError';
+
+export const isServerError = (error: Error): error is ServerError =>
+ error.name === 'ServerError';

MODIFY
src/util/index.ts
src/util/index.ts
-export * from './content-type.util';
-export * from './status-code.util';
+export * from './contentType.util';
+export * from './statusCode.util';

DELETE
src/util/status-code.util.ts
-export enum StatusCode {
- OK = 200,
- NOT_FOUND = 404,
-}

ADD
src/util/statusCode.util.ts
+export enum StatusCode {
+ OK = 200,
+ BAD_REQUEST = 400,
+ UNAUTHORIZED = 401,
+ FORBIDDEN = 403,
+ NOT_FOUND = 404,
+ FAILED_DEPENDENCY = 424,
+ INTERNAL_SERVER_ERROR = 500,
+ SERVICE_UNAVAILABLE = 503,
+}

feature - error catcher middleware