Skip to content

Commit

Permalink
Merge pull request quarkusio#44816 from geoand/quarkusio#44813
Browse files Browse the repository at this point in the history
Introduce property to disable default mapper per REST Client
  • Loading branch information
geoand authored Dec 2, 2024
2 parents 2c31260 + 6ca76ec commit c10465e
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 9 deletions.
28 changes: 28 additions & 0 deletions docs/src/main/asciidoc/rest-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,34 @@ public interface EchoClient {
}
----

=== Disabling the default mapper

As mandated by the REST Client specification, a default exception mapper is included, that throws an exception when HTTP status code is higher than 400.
While this behavior is fine when the client returns a regular object, it is however very unintuitive when the client needs to return a `jakarta.ws.rs.core.Response`
(with the intention of allowing the caller to decide how to handle the HTTP status code).

For this reason, the REST Client includes a property named `disable-default-mapper` which can be used to disable the default mapper when using a REST client in a declarative manner.

For example, with a client like so:

[source, java]
----
@Path("foo")
@RegisterRestClient(configKey = "bar")
public interface Client {
@GET
Response get();
}
----

The default exception mapper can be disabled by setting `quarkus.rest-client.bar.disable-default-mapper=true` to disable the exception mapper for the REST Client configured with the key `bar`.

[NOTE]
====
When using the programmatic approach for creating a REST Client, `QuarkusRestClientBuilder` provides a method named `disableDefaultMapper`
that provides the same feature.
====

[[multipart]]
== Multipart Form support

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -594,8 +594,7 @@ default Optional<String> uriReload() {
Optional<Boolean> http2();

/**
* The max HTTP ch
* unk size (8096 bytes by default).
* The max HTTP chunk size (8096 bytes by default).
* <p>
* This property is not applicable to the RESTEasy Client.
*/
Expand All @@ -615,6 +614,14 @@ default Optional<String> uriReload() {
* This stacktrace will be used if the invocation throws an exception
*/
Optional<Boolean> captureStacktrace();

/**
* If set to {@code true}, then this REST Client will not the default exception mapper which
* always throws an exception if HTTP response code >= 400.
* This property is not applicable to the RESTEasy Client.
*/
@WithDefault("${microprofile.rest.client.disable.default.mapper:false}")
Boolean disableDefaultMapper();
}

class RestClientKeysProvider implements Supplier<Iterable<String>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.quarkus.rest.client.reactive.error;

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

import java.net.URI;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;

public class GlobalExceptionMapperDisabledTest {

@RegisterExtension
static final QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(Client.class, Resource.class))
.overrideRuntimeConfigKey("quarkus.rest-client.test.url", "${test.url}")
.overrideRuntimeConfigKey("microprofile.rest.client.disable.default.mapper", "true");

@RestClient
Client client;

@TestHTTPResource
URI baseUri;

@Test
void testDeclarativeClient() {
Response response = client.get();
assertThat(response.getStatus()).isEqualTo(404);
}

@Test
void testProgrammaticClient() {
OtherClient otherClient = QuarkusRestClientBuilder.newBuilder().baseUri(baseUri).build(OtherClient.class);
Response response = otherClient.get();
assertThat(response.getStatus()).isEqualTo(404);
}

@Path("/error")
public static class Resource {
@GET
public Response returnError() {
return Response.status(404).build();
}
}

@Path("/error")
@RegisterRestClient(configKey = "test")
public interface Client {
@GET
Response get();
}

@Path("/error")
public interface OtherClient {
@GET
Response get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package io.quarkus.rest.client.reactive.error;

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

import java.net.URI;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;

public class PerClientExceptionMapperDisabledTest {

@RegisterExtension
static final QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(Client.class, Resource.class))
.overrideRuntimeConfigKey("quarkus.rest-client.test.url", "${test.url}")
.overrideRuntimeConfigKey("quarkus.rest-client.test.disable-default-mapper", "true")
.overrideRuntimeConfigKey("quarkus.rest-client.test2.url", "${test.url}");

@RestClient
Client client;

@RestClient
Client2 client2;

@TestHTTPResource
URI baseUri;

@Test
void testDeclarativeClient() {
Response response = client.get();
assertThat(response.getStatus()).isEqualTo(404);

assertThatThrownBy(() -> client2.get()).isInstanceOf(WebApplicationException.class);
}

@Test
void testProgrammaticClient() {
OtherClient otherClient = QuarkusRestClientBuilder.newBuilder().baseUri(baseUri).disableDefaultMapper(true)
.build(OtherClient.class);
Response response = otherClient.get();
assertThat(response.getStatus()).isEqualTo(404);

OtherClient otherClient2 = QuarkusRestClientBuilder.newBuilder().baseUri(baseUri).build(OtherClient.class);
assertThatThrownBy(otherClient2::get).isInstanceOf(WebApplicationException.class);
}

@Path("/error")
public static class Resource {
@GET
public Response returnError() {
return Response.status(404).build();
}
}

@Path("/error")
@RegisterRestClient(configKey = "test")
public interface Client {
@GET
Response get();
}

@Path("/error")
@RegisterRestClient(configKey = "test2")
public interface Client2 {
@GET
Response get();
}

@Path("/error")
public interface OtherClient {
@GET
Response get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,12 @@ static QuarkusRestClientBuilder newBuilder() {
*/
QuarkusRestClientBuilder userAgent(String userAgent);

/**
* If set to {@code true}, then this REST Client will not the default exception mapper which
* always throws an exception if HTTP response code >= 400
*/
QuarkusRestClientBuilder disableDefaultMapper(Boolean disable);

/**
* Based on the configured QuarkusRestClientBuilder, creates a new instance of the given REST interface to invoke API calls
* against.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@ public QuarkusRestClientBuilder userAgent(String userAgent) {
return this;
}

@Override
public QuarkusRestClientBuilder disableDefaultMapper(Boolean disable) {
proxy.disableDefaultMapper(disable);
return this;
}

@Override
public <T> T build(Class<T> clazz) throws IllegalStateException, RestClientDefinitionException {
return proxy.build(clazz);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public class RestClientBuilderImpl implements RestClientBuilder {

private Boolean trustAll;
private String userAgent;
private Boolean disableDefaultMapper;

@Override
public RestClientBuilderImpl baseUrl(URL url) {
Expand Down Expand Up @@ -260,6 +261,11 @@ public RestClientBuilderImpl userAgent(String userAgent) {
return this;
}

public RestClientBuilderImpl disableDefaultMapper(Boolean disableDefaultMapper) {
this.disableDefaultMapper = disableDefaultMapper;
return this;
}

@Override
public RestClientBuilderImpl executorService(ExecutorService executor) {
throw new IllegalArgumentException("Specifying executor service is not supported. " +
Expand Down Expand Up @@ -429,13 +435,6 @@ public <T> T build(Class<T> aClass) throws IllegalStateException, RestClientDefi
register(mapper.getKey(), mapper.getValue());
}

Object defaultMapperDisabled = getConfiguration().getProperty(DEFAULT_MAPPER_DISABLED);
Boolean globallyDisabledMapper = ConfigProvider.getConfig()
.getOptionalValue(DEFAULT_MAPPER_DISABLED, Boolean.class).orElse(false);
if (!globallyDisabledMapper && !(defaultMapperDisabled instanceof Boolean && (Boolean) defaultMapperDisabled)) {
exceptionMappers.add(new DefaultMicroprofileRestClientExceptionMapper());
}

exceptionMappers.sort(Comparator.comparingInt(ResponseExceptionMapper::getPriority));
redirectHandlers.sort(Comparator.comparingInt(RedirectHandler::getPriority));
clientBuilder.register(new MicroProfileRestClientResponseFilter(exceptionMappers));
Expand Down Expand Up @@ -483,6 +482,26 @@ public <T> T build(Class<T> aClass) throws IllegalStateException, RestClientDefi
clientBuilder.setUserAgent(restClients.userAgent().get());
}

Boolean effectiveDisableDefaultMapper = disableDefaultMapper;
if (effectiveDisableDefaultMapper == null) {
var configOpt = ConfigProvider.getConfig().getOptionalValue(DEFAULT_MAPPER_DISABLED, Boolean.class);
if (configOpt.isEmpty()) {
// need to support the legacy way where the user does .property("microprofile.rest.client.disable.default.mapper", true)
var defaultMapperDisabledFromProperty = getConfiguration().getProperty(DEFAULT_MAPPER_DISABLED);
if (defaultMapperDisabledFromProperty instanceof Boolean b) {
effectiveDisableDefaultMapper = b;
} else {
effectiveDisableDefaultMapper = false;
}
} else {
effectiveDisableDefaultMapper = configOpt.get();
}
}

if (!effectiveDisableDefaultMapper) {
exceptionMappers.add(new DefaultMicroprofileRestClientExceptionMapper());
}

Integer maxChunkSize = (Integer) getConfiguration().getProperty(QuarkusRestClientProperties.MAX_CHUNK_SIZE);
if (maxChunkSize != null) {
clientBuilder.maxChunkSize(maxChunkSize);
Expand Down Expand Up @@ -573,4 +592,5 @@ private MultiQueryParamMode toMultiQueryParamMode(QueryParamStyle queryParamStyl
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ private void configureCustomProperties(QuarkusRestClientBuilder builder) {

Boolean captureStacktrace = oneOf(restClientConfig.captureStacktrace()).orElse(configRoot.captureStacktrace());
builder.property(QuarkusRestClientProperties.CAPTURE_STACKTRACE, captureStacktrace);

builder.disableDefaultMapper(restClientConfig.disableDefaultMapper());
}

private static Function<MemorySize, Integer> intChunkSize() {
Expand Down

0 comments on commit c10465e

Please sign in to comment.