Skip to content

Commit

Permalink
feat: add BEFORE_REQUEST hook
Browse files Browse the repository at this point in the history
  • Loading branch information
pk-work committed Oct 2, 2023
1 parent 450aaa6 commit 4494a30
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 13 deletions.
3 changes: 3 additions & 0 deletions docs/usage/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ hooks.
endpoint for handling. Note: This hook is intended for the implementation of cross-cutting concerns (for example,
logging or global authorization checks). In case this hook handles the provided routing context in the HookContext,
it may not call the hookPromise, to break the chain.
* `BEFORE_REQUEST`: This hook is called for each incoming web request before any other handler gets executed, which
guarantees the execution of this hook. In case this hook handles the provided routing context in the HookContext, it
may not call the hookPromise, to break the chain.
* `BEFORE_SHUTDOWN`: This hook is called before the associated Vert.x instance to a NeonBee object is closed/shut down.
* `NODE_ADDED`: This hook is called when a node has been added to the cluster.
* `NODE_LEFT`: This hook is called when a node has left the cluster.
3 changes: 2 additions & 1 deletion src/main/java/io/neonbee/endpoint/MountableEndpoint.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.neonbee.endpoint;

import static io.neonbee.hook.HookType.ONCE_PER_REQUEST;
import static io.vertx.core.Future.failedFuture;

import java.lang.reflect.InvocationTargetException;
Expand Down Expand Up @@ -97,7 +98,7 @@ public void mount(Vertx vertx, Router rootRouter, Optional<ChainAuthHandler> def
effectiveAuthChainConfig.map(authChainConfig -> ChainAuthHandler.create(vertx, authChainConfig))
.or(() -> defaultAuthHandler).ifPresent(endpointRoute::handler);

endpointRoute.handler(new HooksHandler());
endpointRoute.handler(new HooksHandler(ONCE_PER_REQUEST));

if (LOGGER.isInfoEnabled()) {
LOGGER.info(
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/io/neonbee/hook/HookType.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ public enum HookType {
*/
ONCE_PER_REQUEST,

/**
* This hook is called for each incoming web request before any other handler gets executed, which guarantees the
* execution of this hook.
* <p>
* Note: In case this hook handles the provided {@linkplain #ROUTING_CONTEXT} in the {@linkplain HookContext} it may
* not call the {@code hookPromise}, to break the chain.
* <p>
* Available parameters in the {@linkplain HookContext}:
* <p>
* {@link #ROUTING_CONTEXT}: {@linkplain io.vertx.ext.web.RoutingContext}
*/
BEFORE_REQUEST,

/**
* The shutdown hook is called before the associated Vert.x instance to a NeonBee object is closed / shut down.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.neonbee.internal.handler;

import static io.neonbee.hook.HookType.BEFORE_REQUEST;

import io.vertx.ext.web.handler.PlatformHandler;

/**
* The only purpose of this class is to mask the HooksHandler, which is a USER handler, as a PLATFORM handler. Because
* the handler chain cannot start with a USER handler.
*/
public class BeforeRequestHandler extends HooksHandler implements PlatformHandler {
/**
* Creates a new BeforeRequestHandler that serves as execution point for BEFORE_REQUEST hooks.
*/
public BeforeRequestHandler() {
super(BEFORE_REQUEST);
}
}
20 changes: 16 additions & 4 deletions src/main/java/io/neonbee/internal/handler/HooksHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,32 @@
import io.vertx.ext.web.RoutingContext;

/**
* This handler will trigger the execution of the ONCE_PER_REQUEST hook, preventing execution of the next handlers if
* any is any error occurs.
* This handler will trigger the execution of the passed {@link HookType} and prevents execution of the next handlers if
* any error occurs during the hook execution.
*/
public class HooksHandler implements Handler<RoutingContext> {
private static final LoggingFacade LOGGER = LoggingFacade.create();

private final HookType hookType;

/**
* Creates a new HooksHandler with the passed HookType.
*
* @param hookType the HookType to execute inside this handler
*/
public HooksHandler(HookType hookType) {
this.hookType = hookType;
}

@Override
public void handle(RoutingContext routingContext) {
NeonBee.get(routingContext.vertx()).getHookRegistry()
.executeHooks(HookType.ONCE_PER_REQUEST, Map.of(ROUTING_CONTEXT, routingContext))
.executeHooks(hookType, Map.of(ROUTING_CONTEXT, routingContext))
.onComplete(asyncResult -> {
if (asyncResult.failed()) {
Throwable cause = asyncResult.cause();
LOGGER.error("An error has occurred while executing the request hook", cause);
LOGGER.error("An error has occurred while executing the request hook of type {}", hookType,
cause);
if (cause instanceof DataException) {
routingContext.fail(((DataException) cause).failureCode());
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.neonbee.endpoint.Endpoint;
import io.neonbee.endpoint.MountableEndpoint;
import io.neonbee.handler.ErrorHandler;
import io.neonbee.internal.handler.BeforeRequestHandler;
import io.neonbee.internal.handler.ChainAuthHandler;
import io.neonbee.internal.handler.DefaultErrorHandler;
import io.neonbee.internal.handler.NotFoundHandler;
Expand Down Expand Up @@ -85,6 +86,9 @@ private Future<Router> createRouter(ServerConfig config) {
// sequence issues, block scope the variable to prevent using it after the endpoints have been mounted
Route rootRoute = router.route();

// The first handler added to the route must be the "BEFORE_REQUEST" HooksHandler.
rootRoute.handler(new BeforeRequestHandler());

return createErrorHandler(config.getErrorHandlerClassName(), vertx).onSuccess(rootRoute::failureHandler)
.compose(unused -> {
List<Future<Handler<RoutingContext>>> handlerFutures =
Expand Down
60 changes: 52 additions & 8 deletions src/test/java/io/neonbee/internal/handler/HooksHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ class HooksHandlerTest extends DataVerticleTestBase {
@Test
@DisplayName("Checks that HooksHandler executes hook")
void checkHookGetsExecutedSuccess(VertxTestContext testContext) {
Checkpoint cp = testContext.checkpoint(2);
Checkpoint cp = testContext.checkpoint(3);
TestHook hook = new TestHook();
hook.doSomething = p -> {
hook.executeBR = p -> {
cp.flag();
p.complete();
};

hook.executeOPR = p -> {
cp.flag();
p.complete();
};
Expand All @@ -43,9 +48,14 @@ void checkHookGetsExecutedSuccess(VertxTestContext testContext) {
@Test
@DisplayName("Checks that HooksHanlder executes hook and fail request in case of error")
void checkHookGetsExecutedAndFailsRequestInCaseOfException(VertxTestContext testContext) {
Checkpoint cp = testContext.checkpoint(2);
Checkpoint cp = testContext.checkpoint(3);
TestHook hook = new TestHook();
hook.doSomething = p -> {
hook.executeBR = p -> {
cp.flag();
p.complete();
};

hook.executeOPR = p -> {
cp.flag();
p.fail(new Exception("Hodor"));
};
Expand All @@ -61,9 +71,14 @@ void checkHookGetsExecutedAndFailsRequestInCaseOfException(VertxTestContext test
@Test
@DisplayName("Checks that HooksHanlder executes hook and propagate error code in case of DataException")
void checkHookGetsExecutedAndPropagateErrorCodeIfDataException(VertxTestContext testContext) {
Checkpoint cp = testContext.checkpoint(2);
Checkpoint cp = testContext.checkpoint(3);
TestHook hook = new TestHook();
hook.doSomething = p -> {
hook.executeBR = p -> {
cp.flag();
p.complete();
};

hook.executeOPR = p -> {
cp.flag();
p.fail(new DataException(403, "Hodor"));
};
Expand All @@ -76,17 +91,46 @@ void checkHookGetsExecutedAndPropagateErrorCodeIfDataException(VertxTestContext
})));
}

@Test
@DisplayName("Checks that ONCE_PER_REQUEST hook isn't called if BEFORE_REQUEST hook fails the request in case of error")
void checkOnePerRequestHookIsntCalledInCaseBeforeRequestFailsTheRequest(VertxTestContext testContext) {
Checkpoint cp = testContext.checkpoint(2);
TestHook hook = new TestHook();
hook.executeBR = p -> {
cp.flag();
p.fail(new Exception("Hodor"));
};

hook.executeOPR = p -> testContext.failNow("Must not be called");

getNeonBee().getHookRegistry().registerInstanceHooks(hook, CORRELATION_ID)
.compose(v -> sendRequestReturnStatusCode())
.onComplete(testContext.succeeding(statusCode -> testContext.verify(() -> {
assertThat(statusCode).isEqualTo(500);
cp.flag();
})));
}

private Future<Integer> sendRequestReturnStatusCode() {
return createRequest(HttpMethod.GET, "/raw/core/Hodor").send().map(response -> response.statusCode());
}

public static class TestHook {
private Consumer<Promise<Void>> doSomething = p -> {};
private Consumer<Promise<Void>> executeOPR = p -> {};

private Consumer<Promise<Void>> executeBR = p -> {};

@SuppressWarnings("PMD.UnusedFormalParameter")
@Hook(HookType.ONCE_PER_REQUEST)
public void test(NeonBee neonBee, HookContext hookContext, Promise<Void> promise) {
doSomething.accept(promise);
executeOPR.accept(promise);
promise.complete();
}

@SuppressWarnings("PMD.UnusedFormalParameter")
@Hook(HookType.BEFORE_REQUEST)
public void test2(NeonBee neonBee, HookContext hookContext, Promise<Void> promise) {
executeBR.accept(promise);
promise.complete();
}
}
Expand Down

0 comments on commit 4494a30

Please sign in to comment.