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

Rest Client Reactive: Add reactive flavor for ClientHeadersFactory #21807

Merged
merged 1 commit into from
Nov 30, 2021
Merged
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
33 changes: 32 additions & 1 deletion docs/src/main/asciidoc/rest-client-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ More details about this can be found in https://smallrye.io/smallrye-mutiny/#_un

There are a few ways in which you can specify custom headers for your REST calls:

- by registering a `ClientHeadersFactory` with the `@RegisterClientHeaders` annotation
- by registering a `ClientHeadersFactory` or a `ReactiveClientHeadersFactory` with the `@RegisterClientHeaders` annotation
- by specifying the value of the header with `@ClientHeaderParam`
- by specifying the value of the header by `@HeaderParam`

Expand Down Expand Up @@ -591,6 +591,37 @@ To specify a value for `${header.value}`, simply put the following in your `appl
header.value=value of the header
----

Also, there is a reactive flavor of `ClientHeadersFactory` that allows doing blocking operations. For example:

[source, java]
----
package org.acme.rest.client;

import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import java.util.UUID;

@ApplicationScoped
public class GetTokenReactiveClientHeadersFactory extends ReactiveClientHeadersFactory {

@Inject
Service service;

@Override
public Uni<MultivaluedMap<String, String>> getHeaders(MultivaluedMap<String, String> incomingHeaders) {
return Uni.createFrom().item(() -> {
MultivaluedHashMap<String, String> newHeaders = new MultivaluedHashMap<>();
// perform blocking call
newHeaders.add(HEADER_NAME, service.getToken());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,
I am sorry that I kind of hijack this already merged PR, but IMHO the ReactiveClientHeadersFactory does not work as expected.
Why has the service.getToken() to be a blocking call?
In my case I wanted to call the auth server where I obtain a token also in a reactive manner by using an reactive rest client.
Therefore my code would look like this:

    override fun getHeaders(incomingHeaders: MultivaluedMap<String, String>?): Uni<MultivaluedMap<String, String>> {
        // Utilizing the reactive mutiny api :-), which unfortunately does not work....
        return clientTokenFetcher.getClientToken().onItem().transform {
            MultivaluedHashMap<String, String>().apply {
                add(HttpHeaders.CACHE_CONTROL, "no-cache")
                add(HttpHeaders.CONTENT_TYPE, "text/plain")
                add(HttpHeaders.AUTHORIZATION, it.authHeader)
            }
        }
    }

Unfortunately this does not work, while this is working:

    override fun getHeaders(incomingHeaders: MultivaluedMap<String, String>?): Uni<MultivaluedMap<String, String>> {
        return Uni.createFrom().item(
            MultivaluedHashMap<String, String>().apply {
                add(HttpHeaders.CACHE_CONTROL, "no-cache")
                add(HttpHeaders.CONTENT_TYPE, "text/plain")
                // Make this a blocking call ;-(
                add(HttpHeaders.AUTHORIZATION, clientTokenFetcher.getClientToken().await().atMost(Duration.ofMillis(500)).authHeader)
            }
        )
    }

Can someone please tell me how to stay fully reactive and make it work without the blocking within the Uni?

The code above is written in kotlin, but I could also convert it into Java if that helps you...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service.getToken() does not have to be a blocking call. This example was only intended to explain how to inject headers from a blocking service to Resteasy Rest Client.

Also, I tried to reproduce your scenario and it worked for me:

    @ApplicationScoped
    public static class Service {
        public Uni<String> getValue() {
            return Uni.createFrom().item(HEADER_VALUE);
        }
    }

    @RegisterClientHeaders(CustomReactiveClientHeadersFactory.class)
    public interface Client {
        @GET
        String getWithHeader();
    }

    public static class CustomReactiveClientHeadersFactory extends ReactiveClientHeadersFactory {

        @Inject
        Service service;

        @Override
        public Uni<MultivaluedMap<String, String>> getHeaders(MultivaluedMap<String, String> incomingHeaders) {
            return service.getValue().onItem().transform(value -> {
                MultivaluedHashMap<String, String> newHeaders = new MultivaluedHashMap<>();
                newHeaders.add(HEADER_NAME, value);
                return newHeaders;
            });
        }
    }

This is verified using this test https://github.com/quarkusio/quarkus/blob/main/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/ReactiveClientHeadersFromProviderTest.java

Therefore, it would be good to open an issue with a reproducer, so we can exactly see what is going on.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Sgitario ,

thanks for your quick response:
Here you can find an example which reproduces my issue:
https://github.com/SimonScholz/ReactiveClientHeadersFactory/blob/b1a7a07c13afa9168b8897653fa27ce0f4cf3be3/src/test/kotlin/io/github/simonscholz/ReactiveResourceTest.kt#L82

In this example I'd use a reactive rest client in order to obtain a token, which is then used by the BeverageRequestHeaderFactory to generate a proper Authorization header.
The ReactiveResourceTest shows that this does not work.
I'll also create an issue later on, but wanted to inform you about my example in advance.

Thanks and regards,

Simon

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Sgitario,

FYI, I've now finally created an issue here: #24153

Please let me know, if I can do more or if you have any questions.

return newHeaders;
});
}
}
----

=== Default header factory

The `@RegisterClientHeaders` annotation can also be used without any custom factory specified. In that case the `DefaultClientHeadersFactoryImpl` factory will be used.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.quarkus.rest.client.reactive.headers;

import static org.assertj.core.api.Assertions.assertThat;

import java.net.URI;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;

import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.rest.client.reactive.ReactiveClientHeadersFactory;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.mutiny.Uni;

public class ReactiveClientHeadersFromProviderTest {
private static final String HEADER_NAME = "my-header";
private static final String HEADER_VALUE = "oifajrofijaeoir5gjaoasfaxcvcz";

@TestHTTPResource
URI baseUri;

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar.addClasses(ReactiveClientHeadersFromProviderTest.Client.class)
.addAsResource(
new StringAsset("my.property-value=" + HEADER_VALUE),
"application.properties"));

@Test
void shouldSetHeaderFromProperties() {
ReactiveClientHeadersFromProviderTest.Client client = RestClientBuilder.newBuilder().baseUri(baseUri)
.build(ReactiveClientHeadersFromProviderTest.Client.class);

assertThat(client.getWithHeader()).isEqualTo(HEADER_VALUE);
}

@Path("/")
@ApplicationScoped
public static class Resource {
@GET
public String returnHeaderValue(@HeaderParam(HEADER_NAME) String header) {
return header;
}
}

@ApplicationScoped
public static class Service {
@Blocking
public String getValue() {
return HEADER_VALUE;
}
}

@RegisterClientHeaders(CustomReactiveClientHeadersFactory.class)
public interface Client {
@GET
String getWithHeader();
}

public static class CustomReactiveClientHeadersFactory extends ReactiveClientHeadersFactory {

@Inject
Service service;

@Override
public Uni<MultivaluedMap<String, String>> getHeaders(MultivaluedMap<String, String> incomingHeaders) {
return Uni.createFrom().item(() -> {
MultivaluedHashMap<String, String> newHeaders = new MultivaluedHashMap<>();
newHeaders.add(HEADER_NAME, service.getValue());
return newHeaders;
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.rest.client.reactive;

import javax.ws.rs.core.MultivaluedMap;

import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;

import io.smallrye.mutiny.Uni;

/**
* Reactive ClientHeadersFactory flavor for Quarkus rest-client reactive extension.
*/
public abstract class ReactiveClientHeadersFactory implements ClientHeadersFactory {
michalszynkiewicz marked this conversation as resolved.
Show resolved Hide resolved
public abstract Uni<MultivaluedMap<String, String>> getHeaders(MultivaluedMap<String, String> incomingHeaders);

@Override
public final MultivaluedMap<String, String> update(MultivaluedMap<String, String> incomingHeaders,
MultivaluedMap<String, String> clientOutgoingHeaders) {
throw new RuntimeException(
"Can't call `update` method in a Reactive context. Use `getHeaders` or implement ClientHeadersFactory.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@

import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;
import org.eclipse.microprofile.rest.client.ext.DefaultClientHeadersFactoryImpl;
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext;

import io.quarkus.arc.Arc;
import io.quarkus.rest.client.reactive.HeaderFiller;
import io.quarkus.rest.client.reactive.ReactiveClientHeadersFactory;

@Priority(Integer.MIN_VALUE)
public class MicroProfileRestClientRequestFilter implements ClientRequestFilter {
Expand Down Expand Up @@ -63,7 +65,20 @@ public void filter(ClientRequestContext requestContext) {
}

if (clientHeadersFactory != null) {
incomingHeaders = clientHeadersFactory.update(incomingHeaders, headers);
if (clientHeadersFactory instanceof ReactiveClientHeadersFactory) {
// reactive
ResteasyReactiveClientRequestContext reactiveRequestContext = (ResteasyReactiveClientRequestContext) requestContext;
ReactiveClientHeadersFactory reactiveClientHeadersFactory = (ReactiveClientHeadersFactory) clientHeadersFactory;
reactiveRequestContext.suspend();
MultivaluedMap<String, String> outgoingHeaders = incomingHeaders;
reactiveClientHeadersFactory.getHeaders(incomingHeaders).subscribe().with(newHeaders -> {
outgoingHeaders.putAll(newHeaders);
reactiveRequestContext.resume();
}, reactiveRequestContext::resume);
} else {
// blocking
incomingHeaders = clientHeadersFactory.update(incomingHeaders, headers);
}
}

for (Map.Entry<String, List<String>> headerEntry : incomingHeaders.entrySet()) {
Expand Down