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

Global and Per-client properties to disable default ResponseExceptionMappe #408

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@

import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
import org.jboss.logging.Logger;
import org.jboss.resteasy.client.jaxrs.internal.ClientRequestContextImpl;
import org.jboss.resteasy.client.jaxrs.internal.ClientResponse;
import org.jboss.resteasy.client.jaxrs.internal.ClientResponseContextImpl;

/**
* This implementation is a bit of a hack and dependent on Resteasy internals.
* We throw a ResponseProcessingExceptoin that hides the Response object
* We throw a ResponseProcessingException that hides the Response object
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public class ExceptionMapping implements ClientResponseFilter {
Expand Down Expand Up @@ -71,18 +72,24 @@ public void mapException(final Method method) throws Exception {
// falling through to here means no applicable exception mapper found
// or applicable mapper returned null
LOGGER.warnf("No default ResponseExceptionMapper found or user's ResponseExceptionMapper returned null."
+ " Response status: %s messge: %s", handled.getStatus(), handled.getReasonPhrase());
+ " Response status: %s message: %s", handled.getStatus(), handled.getReasonPhrase());

}
}

public ExceptionMapping(final Set<Object> instances) {
public ExceptionMapping(final Set<Object> instances, final boolean doNotThrowWhenResponseIsResult) {
this.instances = instances;
this.doNotThrowWhenResponseIsResult = doNotThrowWhenResponseIsResult;
}

@Override
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) {

if (doNotThrowWhenResponseIsResult) {
Method method = ((ClientRequestContextImpl) requestContext).getInvocation().getClientInvoker().getMethod();
if (method.getReturnType().equals(Response.class)) {
return;
}
}
Response response = new PartialResponse(responseContext);

List<ResponseExceptionMapper> candidates = new LinkedList<>();
Expand Down Expand Up @@ -114,4 +121,5 @@ public void filter(ClientRequestContext requestContext, ClientResponseContext re
}

private final Set<Object> instances;
private final boolean doNotThrowWhenResponseIsResult;
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public RestClientBuilder followRedirects(boolean followRedirect) {
}

public boolean isFollowRedirects() {
return this.followRedirect;
return followRedirect == Boolean.TRUE;
}

@Override
Expand Down Expand Up @@ -271,11 +271,14 @@ public <T> T build(Class<T> aClass, ClientHttpEngine httpEngine)
}

// Default exception mapper
if (!isMapperDisabled()) {
if (!isMapperDisabled(aClass)) {
register(DefaultResponseExceptionMapper.class);
}

builderDelegate.register(new ExceptionMapping(localProviderInstances), 1);
Optional<Boolean> prop = rcProperty(aClass, "microprofile.rest.client.disable.response.exceptions",
"disableResponseExceptions", Boolean.class);
boolean doNotThrowWhenResponseIsResult = prop.orElse(Boolean.FALSE);
builderDelegate.register(new ExceptionMapping(localProviderInstances, doNotThrowWhenResponseIsResult), 1);

ClassLoader classLoader = getClassLoader(aClass);

Expand Down Expand Up @@ -345,7 +348,7 @@ public <T> T build(Class<T> aClass, ClientHttpEngine httpEngine)
resteasyClientBuilder.setIsTrustSelfSignedCertificates(false);
checkQueryParamStyleProperty(aClass);
checkFollowRedirectProperty(aClass);
resteasyClientBuilder.setFollowRedirects(followRedirect);
resteasyClientBuilder.setFollowRedirects(isFollowRedirects());

if (readTimeout != null) {
resteasyClientBuilder.readTimeout(readTimeout, readTimeoutUnit);
Expand Down Expand Up @@ -443,92 +446,65 @@ private Optional<InetSocketAddress> selectHttpProxy() {
.findFirst();
}

private <T> Optional<T> rcProperty(Class<?> intfc, String rcKey, String mpKey, Class<T> pCls) {
Optional<T> prop = mpProperty(intfc, mpKey, pCls);
if (prop.isEmpty()) {
// set through config api (client property has precedence over global config)
try {
T value = pCls.cast(builderDelegate.getConfiguration().getProperty(rcKey));
prop = Optional.ofNullable(value);
} catch (ClassCastException e) {
// ignore cast exception
}
if (prop.isEmpty() && config != null) {
prop = config.getOptionalValue(rcKey, pCls);
}
}
return prop;
}

private <T> Optional<T> mpProperty(Class<?> intfc, String key, Class<T> pCls) {
if (config == null) {
return Optional.empty();
}

// property using fully-qualified class name takes precedence
Optional<T> prop = config.getOptionalValue(intfc.getName() + "/mp-rest/" + key, pCls);
if (prop.isPresent()) {
return prop;
}
RegisterRestClient registerClient = intfc.getAnnotation(RegisterRestClient.class);
if (registerClient != null &&
registerClient.configKey() != null &&
!registerClient.configKey().isEmpty()) {
//property using configKey
prop = config.getOptionalValue(registerClient.configKey() + "/mp-rest/" + key, pCls);
}
return prop;
}

private void checkQueryParamStyleProperty(Class<?> aClass) {
// User's programmatic setting takes precedence over
// microprofile-config.properties.
if (queryParamStyle == null) {
if (config != null) {
// property using fully-qualified class name takes precedence
Optional<String> prop = config.getOptionalValue(
aClass.getName() + "/mp-rest/queryParamStyle", String.class);
if (prop.isPresent()) {
queryParamStyle(QueryParamStyle.valueOf(
prop.get().trim().toUpperCase()));

} else {
RegisterRestClient registerRestClient = (RegisterRestClient) aClass.getAnnotation(RegisterRestClient.class);
if (registerRestClient != null &&
registerRestClient.configKey() != null &&
!registerRestClient.configKey().isEmpty()) {

//property using configKey
prop = config.getOptionalValue(registerRestClient.configKey()
+ "/mp-rest/queryParamStyle", String.class);
if (prop.isPresent()) {
queryParamStyle(QueryParamStyle.valueOf(
prop.get().trim().toUpperCase()));
}
}
}
}
}
if (queryParamStyle == null) {
queryParamStyle = QueryParamStyle.MULTI_PAIRS;
Optional<String> prop = mpProperty(aClass, "queryParamStyle", String.class);
queryParamStyle = prop.map(s -> QueryParamStyle.valueOf(s.trim().toUpperCase()))
.orElse(QueryParamStyle.MULTI_PAIRS);
}
}

private void checkFollowRedirectProperty(Class<?> aClass) {
// User's programmatic setting takes precedence over
// microprofile-config.properties.
if (!followRedirect) {
if (config != null) {
// property using fully-qualified class name takes precedence
Optional<Boolean> prop = config.getOptionalValue(
aClass.getName() + "/mp-rest/followRedirects", Boolean.class);
if (prop.isPresent()) {
if (prop.get() != followRedirect) {
followRedirects(prop.get());
}
} else {
RegisterRestClient registerRestClient = aClass.getAnnotation(RegisterRestClient.class);
if (registerRestClient != null &&
registerRestClient.configKey() != null &&
!registerRestClient.configKey().isEmpty()) {

//property using configKey
prop = config.getOptionalValue(
registerRestClient.configKey() + "/mp-rest/followRedirects", Boolean.class);
if (prop.isPresent()) {
if (prop.get() != followRedirect) {
followRedirects(prop.get());
}
}
}
}
}
if (followRedirect == null) {
Optional<Boolean> prop = mpProperty(aClass, "followRedirects", Boolean.class);
followRedirect = prop.orElse(null);
}
}

private boolean isMapperDisabled() {
boolean disabled = false;
Optional<Boolean> defaultMapperProp = config == null ? Optional.empty()
: config.getOptionalValue(DEFAULT_MAPPER_PROP, Boolean.class);

// disabled through config api
if (defaultMapperProp.isPresent() && defaultMapperProp.get().equals(Boolean.TRUE)) {
disabled = true;
} else if (!defaultMapperProp.isPresent()) {

// disabled through jaxrs property
try {
Object property = builderDelegate.getConfiguration().getProperty(DEFAULT_MAPPER_PROP);
if (property != null) {
disabled = (Boolean) property;
}
} catch (Throwable e) {
// ignore cast exception
}
}
private boolean isMapperDisabled(Class<?> intfc) {
Optional<Boolean> prop = rcProperty(intfc, DEFAULT_MAPPER_PROP, "disableDefaultMapper", Boolean.class);
boolean disabled = prop.orElse(Boolean.FALSE);
if (disabled) {
LOGGER.debug("The default ResponseExceptionMapper has been disabled");
}
Expand Down Expand Up @@ -885,7 +861,7 @@ private static ClassLoader getClassLoader(final Class<?> type) {
private String keystorePassword;
private HostnameVerifier hostnameVerifier;
private Boolean useURLConnection;
private boolean followRedirect;
private Boolean followRedirect;
private QueryParamStyle queryParamStyle = null;

private final Set<Object> localProviderInstances = new HashSet<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2024 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jboss.resteasy.microprofile.test.client.exception;

import java.net.URL;

import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.core.Response;

import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.resteasy.microprofile.test.client.exception.resource.ProxyExceptionFactory;
import org.jboss.resteasy.microprofile.test.client.exception.resource.ProxyExceptionResource;
import org.jboss.resteasy.microprofile.test.util.TestEnvironment;
import org.jboss.shrinkwrap.api.Archive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(ArquillianExtension.class)
@RunAsClient
public class ResponseDisableExceptionTest {

@ArquillianResource
private URL url;

@Deployment
public static Archive<?> deploySimpleResource() {
return TestEnvironment.createWar(ResponseDisableExceptionTest.class)
.addClasses(ProxyExceptionResource.class);
}

/**
* response and exception mapping
*/
@Test
public void testResponseExceptions() throws Exception {
System.setProperty("microprofile.rest.client.disable.response.exceptions", "true");
try {
ProxyExceptionFactory mpRestClient = RestClientBuilder.newBuilder()
.baseUri(TestEnvironment.generateUri(url, "test-app"))
.build(ProxyExceptionFactory.class);
Assertions.assertThrows(BadRequestException.class,
() -> mpRestClient.asException(400, "Bad Parameter"));
Response response = mpRestClient.asResponse(400, "Bad Parameter");
Assertions.assertEquals(400, response.getStatusInfo().getStatusCode());
} finally {
System.clearProperty("microprofile.rest.client.disable.response.exceptions");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2021 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jboss.resteasy.microprofile.test.client.exception.resource;

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

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@Path("/exception")
@RegisterRestClient
public interface ProxyExceptionFactory {

@GET
Response asResponse(@QueryParam("status") int status, @QueryParam("reason") String reason);

@GET
String asException(@QueryParam("status") int status, @QueryParam("reason") String reason);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2021 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jboss.resteasy.microprofile.test.client.exception.resource;

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

@Path("/exception")
public class ProxyExceptionResource {

@GET
public Response asResponse(@QueryParam("status") int status, @QueryParam("reason") String reason) {
return Response.status(status, reason).build();
}
}
Loading