Skip to content

Commit de3c319

Browse files
authored
Merge pull request #47041 from cescoffier/keep-reference-on-parent-context
Keep a reference on the parent/original context to allow accessing it from Rest Client interceptors
2 parents 58c5782 + 14591a1 commit de3c319

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package io.quarkus.rest.client.reactive;
2+
3+
import static io.restassured.RestAssured.when;
4+
import static org.hamcrest.Matchers.is;
5+
6+
import java.io.IOException;
7+
import java.util.Optional;
8+
9+
import jakarta.ws.rs.GET;
10+
import jakarta.ws.rs.Path;
11+
import jakarta.ws.rs.client.ClientRequestContext;
12+
import jakarta.ws.rs.client.ClientRequestFilter;
13+
import jakarta.ws.rs.client.ClientResponseContext;
14+
import jakarta.ws.rs.client.ClientResponseFilter;
15+
16+
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
17+
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
18+
import org.eclipse.microprofile.rest.client.inject.RestClient;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.extension.RegisterExtension;
21+
22+
import io.quarkus.test.QuarkusUnitTest;
23+
import io.smallrye.common.vertx.ContextLocals;
24+
import io.vertx.core.Context;
25+
import io.vertx.core.Vertx;
26+
27+
public class ContextLocalPropagationTest {
28+
29+
@RegisterExtension
30+
static final QuarkusUnitTest config = new QuarkusUnitTest()
31+
.withApplicationRoot((jar) -> jar.addClasses(Resource.class, Client.class))
32+
.overrideRuntimeConfigKey("quarkus.rest-client.client.url",
33+
"http://localhost:${quarkus.http.test-port:8081}");
34+
35+
@Test
36+
void testQueryParamsWithPrimitiveArrays() {
37+
when().get("test/invokeClient")
38+
.then()
39+
.statusCode(200)
40+
.body(is("test/foo/bar"));
41+
}
42+
43+
@Path("test")
44+
public static class Resource {
45+
46+
private final Client client;
47+
48+
public Resource(@RestClient Client client) {
49+
this.client = client;
50+
}
51+
52+
@Path("invokeClient")
53+
@GET
54+
public String invokeClient() {
55+
var result = client.get();
56+
Optional<String> fromRequest = ContextLocals.get("fromRequest");
57+
Optional<String> fromResponse = ContextLocals.get("fromResponse");
58+
return result + "/" + fromRequest.orElse("none") + "/" + fromResponse.orElse("none");
59+
}
60+
61+
@Path("toClient")
62+
@GET
63+
public String toClient() {
64+
return "test";
65+
}
66+
}
67+
68+
@Path("test")
69+
@RegisterRestClient(configKey = "client")
70+
@RegisterProvider(RequestFilter.class)
71+
@RegisterProvider(ResponseFilter.class)
72+
public interface Client {
73+
74+
@GET
75+
@Path("toClient")
76+
String get();
77+
}
78+
79+
public static class RequestFilter implements ClientRequestFilter {
80+
81+
@Override
82+
public void filter(ClientRequestContext requestContext) throws IOException {
83+
RestClientContextUtil.putLocal("fromRequest", "foo");
84+
}
85+
}
86+
87+
public static class ResponseFilter implements ClientResponseFilter {
88+
89+
@Override
90+
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext)
91+
throws IOException {
92+
RestClientContextUtil.putLocal("fromResponse", "bar");
93+
}
94+
}
95+
96+
public static final class RestClientContextUtil {
97+
98+
private RestClientContextUtil() {
99+
}
100+
101+
public static void putLocal(Object key, Object value) {
102+
determineRestClientContext().putLocal(key, value);
103+
}
104+
105+
private static Context determineRestClientContext() {
106+
// In an ideal world, this would always be populated, however because we never
107+
// defined a proper execution model for the REST Client handlers, currently we are
108+
// in a situation where request filters could be run on the calling context
109+
// and not the client's purpose built context.
110+
// We will need a proper solution soon, but as we need to have a proper way to
111+
// set contextual information in Quarkus 3.20 (LTS), we can't risk breaking
112+
// client code everywhere, so for now we will tell people to check the context
113+
Optional<Object> maybeParentContext = ContextLocals.get("__PARENT_CONTEXT__");
114+
Context effectiveContext;
115+
if (maybeParentContext.isPresent()) {
116+
effectiveContext = (Context) maybeParentContext.get();
117+
} else {
118+
effectiveContext = Vertx.currentContext();
119+
}
120+
return effectiveContext;
121+
}
122+
}
123+
}

independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientRequestContextImpl.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ public ClientRequestContextImpl(RestClientRequestContext restClientRequestContex
6666
// A separate context allows integrations like OTel to create a separate Span for each invocation (expected)
6767
Context current = client.vertx.getOrCreateContext();
6868
this.context = VertxContext.createNewDuplicatedContext(current);
69+
// If an interceptor wants to access the parent context, it can do so by using the "__PARENT_CONTEXT__" key.
70+
// This approach is compatible with the next-to-be nested context support (from SmallRye Common).
71+
// The "__PARENT_CONTEXT__" key will continue to reference the _parent_ context.
72+
// So, currently, an interceptor needing to read from the original context will need to use:
73+
// Context parent = ContextLocals.get("__PARENT_CONTEXT__");
74+
// V v = parent.getLocal("some-key");
75+
// To write to the original context, it will need to use:
76+
// Context parent = ContextLocals.get("__PARENT_CONTEXT__");
77+
// parent.putLocal("some-key", someValue);
78+
// Note that unlike with nested contexts, multiple child contexts can write to the same parent key, and may
79+
// lead to a race condition.
80+
this.context.putLocal("__PARENT_CONTEXT__", current);
6981
restClientRequestContext.properties.put(VERTX_CONTEXT_PROPERTY, context);
7082
}
7183

0 commit comments

Comments
 (0)