@@ -160,8 +160,11 @@ class CqrsEventEmitter extends EventEmitter {
}
export type ErrorHandler = <E extends Error = Error>(err?: E) => never;
interface ServerState {
requestDecorators: Set<RequestDecorator>;
errorHandler: ErrorHandler;
}
export interface Server {
@@ -170,12 +173,17 @@ export interface Server {
close(callback?: (err?: Error) => void): this;
listen(...args: Parameters<http.Server['listen']>): this;
requestDecorator(requestDecorator: RequestDecorator): this;
defaultErrorHandler(): this;
defaultErrorHandler(errorHandler: ErrorHandler ): this;
}
const DEFAULT_ERROR_HANDLER: ErrorHandler = (err) => {
throw err;
};
export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => {
const state: ServerState = {
requestDecorators: new Set<RequestDecorator>(),
errorHandler: DEFAULT_ERROR_HANDLER,
};
const isHttps = 'key' in serverParams && 'cert' in serverParams;
@@ -391,7 +399,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
);
};
const handleMiddlewar eError = (processRequestErrRaw: Error) => (resourceReq: ResourceRequestContext, res: http.ServerResponse<RequestContext>) => {
const handleResourc eError = (processRequestErrRaw: Error) => (resourceReq: ResourceRequestContext, res: http.ServerResponse<RequestContext>) => {
const finalErr = processRequestErrRaw as ErrorPlainResponse;
const headers = finalErr.headers ?? {};
const language = resourceReq.cn.language ?? resourceReq.backend.cn.language;
@@ -439,6 +447,12 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
res.end();
};
const handleError = (err: Error) => (req: RequestContext, res: http.ServerResponse<RequestContext>) => {
if ('resource' in req && typeof req.resource !== 'undefined') {
handleResourceError(err)(req as ResourceRequestContext, res);
}
};
const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => {
const plainReq = await decorateRequest(reqRaw); // TODO add type safety here, put handleGetRoot as its own middleware as it does not concern over any resource
const language = plainReq.cn.language ?? plainReq.backend.cn.language;
@@ -462,7 +476,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
middlewareState = await processRequestFn(resourceReq) as any; // TODO fix this
} catch (processRequestErrRaw) {
// TODO add error handlers
handleMiddleware Error(processRequestErrRaw as Error)(resourceReq, res);
handleError(processRequestErrRaw as Error)(resourceReq, res);
return;
}
@@ -501,23 +515,25 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
return `${mimeType}/merge-patch+${mimeSubtype}`;
}).join(',');
}
res.statusMessage = language.statusMessages['unableToSerializeResponse']?.replace(
/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, headers);
res.end();
handleError(new ErrorPlainResponse('unableToSerializeResponse', {
cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
headers,
res,
}))(resourceReq, res);
return;
}
try {
encoded = charset.encode(serialized);
} catch (cause) {
res.statusMessage = language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': language.name ,
});
res.end( );
handleError(new ErrorPlainResponse('unableToEncodeResponse', {
cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
headers ,
res,
}))(resourceReq, res );
return;
}
@@ -539,30 +555,38 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
}
if (middlewares.length > 0) {
res.statusMessage = language.statusMessages.methodNotAllowed.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED , {
Allow: middlewares.map((m) => m.method).join(', '),
'Content-Language': language.name ,
});
res.end( );
handleError(new ErrorPlainResponse('methodNotAllowed', {
statusCode: constants.HTTP_STATUS_METHOD_NOT_ALLOWED,
res,
headers: {
Allow: middlewares.map((m) => m.method).join(', ') ,
},
}))(resourceReq, res );
return;
}
res.statusMessage = language.statusMessages.urlNotFound.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_NOT_FOUND, {
'Content-Language': language.name,
});
res.end();
handleError(new ErrorPlainResponse('urlNotFound', {
statusCode: constants.HTTP_STATUS_NOT_FOUND,
res,
}))(resourceReq, res);
return;
}
res.statusMessage = language.statusMessages.notImplemented.replace(/\$RESOURCE/g,
reqRaw.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_NOT_IMPLEMENTED, {
'Content-Language': language.name,
});
res.end();
try {
state.errorHandler();
} catch (err) {
handleError(err as Error)(reqRaw, res);
//handleMiddlewareError(err)(reqRaw, res);
return;
}
// TODO default error handling
handleError(
new ErrorPlainResponse('urlNotFound', {
statusCode: constants.HTTP_STATUS_NOT_FOUND,
res,
})
)(reqRaw, res);
};
server.on('request', handleRequest);
@@ -585,7 +609,8 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
state.requestDecorators.add(requestDecorator);
return this;
},
defaultErrorHandler() {
defaultErrorHandler(errorHandler: ErrorHandler) {
state.errorHandler = errorHandler;
return this;
}
} satisfies Server;