Skip to content

Commit

Permalink
Reactive routes - fix request context propagation
Browse files Browse the repository at this point in the history
If there is a route filter and the request can have body (POST, PUT, etc.) then
the route method is invoked asynchronously (once all data are read). However,
the request context is activated by the filter and so we need to make sure the
same context is then used in the route method.

- resolves quarkusio#13073
  • Loading branch information
mkouba committed Nov 25, 2020
1 parent 2a1883b commit 2cda14d
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.quarkus.vertx.web.filter;

import static org.hamcrest.Matchers.is;

import java.util.concurrent.atomic.AtomicInteger;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.RouteFilter;
import io.restassured.RestAssured;
import io.vertx.ext.web.RoutingContext;

public class UserFilterRequestContextPropagationTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(FilterAndRoute.class, RequestFoo.class));

@Test
public void test() {
RestAssured.post("/hello").then().statusCode(200)
.body(is("11"));
}

public static class FilterAndRoute {

@Inject
RequestFoo foo;

@RouteFilter
void filter1(RoutingContext rc) {
foo.setState(11);
rc.next();
}

@Route(path = "hello")
void hello(RoutingContext ctx) {
ctx.response().end("" + foo.getState());
}

}

@RequestScoped
static class RequestFoo {

private AtomicInteger state = new AtomicInteger(-1);

void setState(int value) {
this.state.set(value);
}

public int getState() {
return state.get();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import javax.enterprise.event.Event;

import io.quarkus.arc.Arc;
import io.quarkus.arc.InjectableContext;
import io.quarkus.arc.InjectableContext.ContextState;
import io.quarkus.arc.ManagedContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
Expand All @@ -20,12 +20,16 @@
*/
public abstract class RouteHandler implements Handler<RoutingContext> {

private static final String REQUEST_CONTEXT_STATE = "__cdi_req_ctx";

private final Event<SecurityIdentity> securityIdentityEvent;
private final CurrentVertxRequest currentVertxRequest;
private final ManagedContext requestContext;

public RouteHandler() {
this.securityIdentityEvent = Arc.container().beanManager().getEvent().select(SecurityIdentity.class);
this.currentVertxRequest = Arc.container().instance(CurrentVertxRequest.class).get();
this.requestContext = Arc.container().requestContext();
}

/**
Expand All @@ -38,7 +42,6 @@ public RouteHandler() {
@Override
public void handle(RoutingContext context) {
QuarkusHttpUser user = (QuarkusHttpUser) context.user();
ManagedContext requestContext = Arc.container().requestContext();
//todo: how should we handle non-proactive authentication here?
if (requestContext.isActive()) {
if (user != null) {
Expand All @@ -47,22 +50,31 @@ public void handle(RoutingContext context) {
invoke(context);
} else {
try {
// Activate the context, i.e. set the thread locals
requestContext.activate();
// First attempt to obtain the request context state.
// If there is a route filter and the request can have body (POST, PUT, etc.) the route
// method is invoked asynchronously (once all data are read).
// However, the request context is activated by the filter and we need to make sure
// the same context is then used in the route method
ContextState state = context.get(REQUEST_CONTEXT_STATE);
// Activate the context, i.e. set the thread locals, state can be null
requestContext.activate(state);
currentVertxRequest.setCurrent(context);
if (user != null) {
securityIdentityEvent.fire(user.getSecurityIdentity());
}
// Reactive routes can use async processing (e.g. mutiny Uni/Multi) and context propagation
// 1. Store the state (which is basically a shared Map instance)
// 2. Terminate the context correcly when the response is disposed or an exception is thrown
InjectableContext.ContextState state = requestContext.getState();
context.addEndHandler(new Handler<AsyncResult<Void>>() {
@Override
public void handle(AsyncResult<Void> result) {
requestContext.destroy(state);
}
});
if (state == null) {
// Reactive routes can use async processing (e.g. mutiny Uni/Multi) and context propagation
// 1. Store the state (which is basically a shared Map instance)
// 2. Terminate the context correcly when the response is disposed or an exception is thrown
final ContextState endState = requestContext.getState();
context.put(REQUEST_CONTEXT_STATE, endState);
context.addEndHandler(new Handler<AsyncResult<Void>>() {
@Override
public void handle(AsyncResult<Void> result) {
requestContext.destroy(endState);
}
});
}
invoke(context);
} finally {
// Deactivate the context, i.e. cleanup the thread locals
Expand Down

0 comments on commit 2cda14d

Please sign in to comment.