Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for router specific error handler #328

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/Middlewares.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ server.use(async (request, response) => {
await some_async_operation();
// The request proceeds to the next middleware/handler after the promise resolves
} catch (error) {
return error; // This will trigger global error handler as we are returning an Error
next(error); // This will trigger global error handler as we are returning an Error
//or throw error;
}
});
```
5 changes: 4 additions & 1 deletion docs/Router.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ webserver.use('/api/v1', api_v1_router);
| :-------- | :------- | :------------------------- |
| `routes` | `Array` | Routes contained in this router. |
| `middlewares` | `Array` | Middlewares contained in this router in proper execution order. |
| `handlers` | `Object` | Router specific handlers for current instance |

### Router Instance Methods
* `route(String: pattern)`: Returns a chainable `Router` instance which can be used to chain calls for the same route.
* **Example**: `Router.route('/api/v1').get(getApiV1Handler).post(postApiV1Handler)`
* `set_error_handler(Function: handler)`: Binds a catch-all error handler that will attempt to catch mostsynchronous/asynchronous errors on routes defined on this router.
* **Handler Parameters:** `(Request: request, Response: response, Error: error) => {}`.
* `use(...2 Overloads)`: Binds middlewares and mounts `Router` instances on the optionally specified pattern hierarchy.
* **Overload Types**:
* `use(Function | Router: ...handler)`: Binds the specified functions as middlewares and mounts the `Router` instances on the `/` pattern.
Expand Down Expand Up @@ -89,4 +92,4 @@ webserver.use('/api/v1', api_v1_router);
* `max_payload_length`[`Number`]: Maximum length of allowed incoming messages per connection.
* **Default**: `32 * 1024` > `32,768`
* **Note** any connection that sends a message larger than this number will be immediately closed.
* **See** [`> [Websocket]`](./Websocket.md) for usage documentation on this method and working with websockets.
* **See** [`> [Websocket]`](./Websocket.md) for usage documentation on this method and working with websockets.
2 changes: 1 addition & 1 deletion docs/Server.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Below is a breakdown of the `Server` component which is an extended `Router` ins
* `close(uws_socket?: socket)`: Closes the uWebsockets server instantly dropping all pending requests.
* **Note**: listen_socket is not required.
* **Returns** a `Boolean` representing whether the socket was closed successfully.
* `set_error_handler(Function: handler)`: Binds a global catch-all error handler that will attempt to catch mostsynchronous/asynchronous errors.
* `set_error_handler(Function: handler)`: Binds a global catch-all error handler that will attempt to catch mostsynchronous/asynchronous errors. This is the default error handler if router specific routes do not have a defined error handler.
* **Handler Parameters:** `(Request: request, Response: response, Error: error) => {}`.
* `set_not_found_handler(Function: handler)`: Binds a global catch-all not found handler that will handle all requests which are not handled by any routes.
* **Handler Parameters:** `(Request: request, Response: response) => {}`.
Expand Down
3 changes: 2 additions & 1 deletion src/components/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ class Server extends Router {
*/
_create_route(record) {
// Destructure record into route options
const { method, pattern, options, handler } = record;
const { method, pattern, options, handler, handlers } = record;

// Do not allow route creation once it is locked after a not found handler has been bound
if (this.#routes_locked === true)
Expand All @@ -381,6 +381,7 @@ class Server extends Router {
pattern,
options,
handler,
handlers,
});

// Mark route as temporary if specified from options
Expand Down
9 changes: 7 additions & 2 deletions src/components/http/Response.js
Original file line number Diff line number Diff line change
Expand Up @@ -866,9 +866,14 @@ class Response {
if (!(error instanceof Error)) error = new Error(`ERR_CAUGHT_NON_ERROR_TYPE: ${error}`);

// Trigger the global error handler
this.route.app.handlers.on_error(this._wrapped_request, this, error);
// These have changed to return statements to stop the logic of missing the
// error handler when middleware is mounted on the Server or Router instance.
if (this.route.handlers.on_error === null) {
this.route.app.handlers.on_error(this._wrapped_request, this, error);
} else {
this.route.handlers.on_error(this._wrapped_request, this, error);
}

// Return this response instance
return this;
}

Expand Down
5 changes: 4 additions & 1 deletion src/components/router/Route.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Route {
method = '';
pattern = '';
handler = null;
handlers = null;
options = null;
streaming = null;
max_body_length = null;
Expand All @@ -21,11 +22,12 @@ class Route {
* @param {String} options.pattern - The route pattern.
* @param {Function} options.handler - The route handler.
*/
constructor({ app, method, pattern, options, handler }) {
constructor({ app, method, pattern, options, handler, handlers }) {
this.id = app._get_incremented_id();
this.app = app;
this.pattern = pattern;
this.handler = handler;
this.handlers = handlers;
this.options = options;
this.method = method.toUpperCase();
this.streaming = options.streaming || app._options.streaming || {};
Expand All @@ -46,6 +48,7 @@ class Route {
* @property {Number} id - Unique identifier for this middleware based on it's registeration order.
* @property {String} pattern - The middleware pattern.
* @property {function} handler - The middleware handler function.
* @property {Object} handlers - The on_not_found and on_error handler functions.
* @property {Boolean=} match - Whether to match the middleware pattern against the request path.
*/

Expand Down
51 changes: 44 additions & 7 deletions src/components/router/Router.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ class Router {
const method = arguments[0];

// The pattern, options and handler must be dynamically parsed depending on the arguments provided and router behavior
let pattern, options, handler;
let pattern, options, handler, handlers = null;

// Iterate through the remaining arguments to find the above values and also build an Array of middleware / handler callbacks
// The route handler will be the last one in the array
const callbacks = [];
for (let i = 1; i < arguments.length; i++) {
const argument = arguments[i];

// The second argument should be the pattern. If it is a string, it is the pattern. If it is anything else and we do not have a context pattern, throw an error as that means we have no pattern.
// The router on_error and on_not_found handlers are the last argument in the array
if (i === 1) {
if (typeof argument === 'string') {
if (this.#context_pattern) {
Expand Down Expand Up @@ -98,6 +98,9 @@ class Router {
} else if (Array.isArray(argument)) {
// Scenario: Array of functions
callbacks.push(...argument);
} else if (method !== 'ws' && i === arguments.length - 1) {
// Scenario: Router-specific handlers
handlers = argument;
} else if (argument && typeof argument == 'object') {
// Scenario: Route options object
options = argument;
Expand Down Expand Up @@ -131,12 +134,15 @@ class Router {
// Write the middlewares into the options object
options.middlewares = middlewares;

// Initialize the record object which will hold information about this route
// Initialize the record object which will hold information about this route.
// If the handlers variable is null then default to the Server defined on_error
// and on_not_found handlers.
const record = {
method,
pattern,
options,
handler,
handlers: handlers || this.handlers,
};

// Store record for future subscribers
Expand Down Expand Up @@ -191,13 +197,14 @@ class Router {
const { routes, middlewares } = object;

// Register routes from router locally with adjusted pattern
routes.forEach((record) =>
routes.forEach((record) => {
reference._register_route(
record.method,
merge_relative_paths(pattern, record.pattern),
record.options,
record.handler
)
record.handler,
record.handlers,
)}
);

// Register middlewares from router locally with adjusted pattern
Expand All @@ -210,7 +217,8 @@ class Router {
object.method,
merge_relative_paths(pattern, object.pattern),
object.options,
object.handler
object.handler,
object.handlers,
);
case 'middleware':
// Register middleware from router locally with adjusted pattern
Expand Down Expand Up @@ -238,6 +246,26 @@ class Router {
this.#subscribers.push(handler);
}

#handlers = {
on_error: null,
};

/**
* @typedef RouteErrorHandler
* @type {function(Request, Response, Error):void}
*/

/**
* Sets a router error handler which will catch most uncaught errors across all routes/middlewares on this
* router.
*
* @param {RouteErrorHandler} handler
*/
set_error_handler(handler) {
if (typeof handler !== 'function') throw new Error('HyperExpress: handler must be a function');
this.#handlers.on_error = handler;
}

/**
* Registers middlewares and router instances on the specified pattern if specified.
* If no pattern is specified, the middleware/router instance will be mounted on the '/' root path by default of this instance.
Expand Down Expand Up @@ -478,6 +506,15 @@ class Router {
get middlewares() {
return this.#records.middlewares;
}

/**
* Router instance handlers.
* @returns {Object}
*/
get handlers() {
return this.#handlers;
}

}

module.exports = Router;
1 change: 1 addition & 0 deletions src/shared/operators.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* @param {Object} obj2 Focus Object
*/
function wrap_object(original, target) {
if (!target) return;
Object.keys(target).forEach((key) => {
if (typeof target[key] == 'object') {
if (Array.isArray(target[key])) return (original[key] = target[key]); // lgtm [js/prototype-pollution-utility]
Expand Down
Loading