//--------------------------------------------------------
//-- Node IoC - HTTP - Services - Handler
//--------------------------------------------------------
import __ from '@absolunet/private-registry';
import checksTypes from '../../support/mixins/checksTypes';
/**
* Route handler that handle all the pipeline from request to response.
*
* @memberof http.services
* @augments support.mixins.CheckTypes
* @hideconstructor
*/
class Handler extends checksTypes() {
/**
* Class dependencies: <code>['app', 'config', 'http.error.mapper', 'router.controller', 'router.route']</code>.
*
* @type {Array<string>}
*/
static get dependencies() {
return (super.dependencies || []).concat(['app', 'config', 'http.error.mapper', 'router.controller', 'router.route']);
}
/**
* Handle HTTP request.
*
* @param {http.Route} route - Current route instance.
* @param {request} request - Current request instance.
* @param {response} response - Current response instance.
* @returns {Promise<response>} The processed response.
*/
async handleRequest(route, request, response) {
this.prepareHandling({ route, request, response });
try {
if (this.isFunction(route.action)) {
await this.handleRequestWithClosure();
} else {
await this.handleRequestWithController();
}
} catch (error) {
await this.handleRequestException(error);
}
await this.terminateHandling();
return response;
}
/**
* Handle request when the route was not found.
*
* @param {request} request - Current request instance.
* @param {response} response - Current response instance.
* @returns {Promise<response>} The processed response.
*/
handleRouteNotFound(request, response) {
const action = () => {
throw this.getErrorInstanceFromHttpStatus(this.routes.findByPath(request.url).length > 0 ? 405 : 404);
};
return this.handleRequest({ action }, request, response);
}
/**
* Handle current request with give promise or result.
*
* @param {Promise<*>|*} promise - The current request process.
* @returns {Promise} The async process promise.
*/
async handleRequestWith(promise) {
await Promise.race([promise, this.getHttpTimeoutPromise()]);
}
/**
* Handle current request with closure attached to route.
*
* @returns {Promise} The async process promise.
*/
async handleRequestWithClosure() {
await this.handleRequestWith(this.route.action(this.request, this.response));
}
/**
* Handle current request with controller attached to route.
*
* @returns {Promise} The async process promise.
*/
async handleRequestWithController() {
await this.handleRequestWith(this.callControllerAction());
}
/**
* Get an internal call result handler to either resolve or
* reject the given request based on received HTTP code.
*
* @param {Function} resolve - The promise resolving.
* @param {Function} reject - The promise rejection.
* @returns {Function} The internal call result handler.
*/
getInternalCallResultHandler(resolve, reject) {
return (code, data) => {
const payload = { code, data };
if (code >= 200 && code < 400) {
return resolve(payload);
}
return reject(payload);
};
}
/**
* Prepare request handling.
*
* @param {{route: Route, request: request, response: response}} objects - The current route, request and response instances.
*/
prepareHandling(objects) {
__(this).set('handling', objects);
}
/**
* Terminate request handling.
*
* @returns {response} The current response.
*/
async terminateHandling() {
const { exceptionHandler, response } = this;
if (!response.statusCode) {
response.status(exceptionHandler.hadException ? 500 : 200);
}
const { statusCode } = response;
if ((statusCode < 200 || statusCode >= 400) && !exceptionHandler.hadException) {
await this.handleRequestException(this.getErrorInstanceFromHttpStatus(statusCode));
}
__(this).set('handling', {});
if (response.headersSent) {
response.end();
} else {
await new Promise((resolve) => {
response.on('finish', () => {
resolve();
});
});
}
}
/**
* Handle exception that occurred during request handling.
*
* @param {Error} exception - The throw exception.
* @returns {Promise} The async process promise.
*/
async handleRequestException(exception) {
await this.exceptionHandler.handle(exception, this.request, this.response);
}
/**
* Call route controller action.
*
* @returns {Promise<*>|*} The request handling process.
*/
callControllerAction() {
const { action: name, defaults } = this.route;
const action = this.resolveControllerAction(name);
return action(defaults);
}
/**
* Resolve controller action method.
*
* @returns {Function} The bound controller method.
*/
resolveControllerAction() {
const { action: name } = this.route;
const controller = this.routerController.get(name);
const controllerMethod = this.routerController.resolveAction(name);
const action = controller[controllerMethod];
if (!this.isFunction(action)) {
return this.throwControllerActionNotFound(name);
}
controller.prepareHandling(this.app, this.request, this.response);
return action.bind(controller);
}
/**
* Get HTTP timeout promise.
* This promise will be rejected after a configured time lapse.
*
* @returns {Promise<Error>} The promise of a timeout error.
*/
getHttpTimeoutPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(this.getHttpTimeoutException());
}, this.config.get('http.timeout', 30) * 1000);
});
}
/**
* Get HTTP timeout exception.
*
* @returns {http.exceptions.TimeoutHttpError} The timeout error.
*/
getHttpTimeoutException() {
return this.getErrorInstanceFromHttpStatus(408);
}
/**
* Throw custom TypeError indicating that the controller action was not found.
*
* @param {string} controller - The controller action.
* @throws {TypeError} Indicates that the action was not found in the given controller.
*/
throwControllerActionNotFound(controller) {
const name = this.routerController.resolveName(controller);
const method = this.routerController.resolveAction(controller);
throw new TypeError(`Action "${method}" in controller "${name}" does not exists.`);
}
/**
* Get HTTP error that matches the given status.
*
* @param {number} status - The HTTP status code.
* @returns {http.exceptions.HttpError} The HTTP Error that matches the status, or generic error.
*/
getErrorInstanceFromHttpStatus(status) {
return this.httpErrorMapper.getErrorInstanceFromHttpStatus(status);
}
/**
* The current route.
*
* @type {http.Route}
*/
get route() {
return __(this).get('handling').route;
}
/**
* Route repository.
*
* @type {http.repositories.RouteRepository}
*/
get routes() {
return this.routerRoute;
}
/**
* The current request.
*
* @type {request}
*/
get request() {
return __(this).get('handling').request;
}
/**
* The current response.
*
* @type {response}
*/
get response() {
return __(this).get('handling').response;
}
/**
* Application exception handler.
*
* @type {foundation.exceptions.Handler}
*/
get exceptionHandler() {
return this.app.make('exception.handler');
}
}
export default Handler;