Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public BasicAuthRequestInterceptor(String username, String password) {

@Override
public void apply(RequestTemplate template) {
template.header(AUTHORIZATION_HEADER, BASIC_AUTH_PREFIX + credentials);
if (!template.headers().containsKey(AUTHORIZATION_HEADER)) {
template.header(AUTHORIZATION_HEADER, BASIC_AUTH_PREFIX + credentials);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.fineract.client.feign.support.ApiResponseDecoder;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
Expand Down Expand Up @@ -95,7 +96,8 @@ public <T> T createClient(Class<T> apiType) {
Encoder multipartEncoder = new FineractMultipartEncoder(jacksonEncoder);

return Feign.builder().client(getOrCreateHttpClient()).encoder(multipartEncoder)
.decoder(new JacksonDecoder(ObjectMapperFactory.getShared())).errorDecoder(new FineractErrorDecoder())
.decoder(new ApiResponseDecoder(new JacksonDecoder(ObjectMapperFactory.getShared())))
.errorDecoder(new FineractErrorDecoder())
.options(new Request.Options(connectTimeout, TimeUnit.MILLISECONDS, readTimeout, TimeUnit.MILLISECONDS, true))
.retryer(Retryer.NEVER_RETRY).requestInterceptor(new BasicAuthRequestInterceptor(username, password))
.requestInterceptor(new TenantIdRequestInterceptor(tenantId)).logger(new Slf4jLogger(apiType))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.fineract.client.feign.support;

import feign.Response;
import feign.codec.Decoder;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import org.apache.fineract.client.models.ApiResponse;

/**
* Custom Feign decoder that handles ApiResponse&lt;T&gt; return types for *WithHttpInfo methods.
*
* When a Feign client method returns ApiResponse&lt;T&gt;, the standard JacksonDecoder cannot handle it because:
* <ul>
* <li>The server returns just T (the body), not ApiResponse&lt;T&gt;</li>
* <li>ApiResponse wraps the body with HTTP status code and headers</li>
* </ul>
*
* This decoder:
* <ol>
* <li>Detects if the return type is ApiResponse&lt;T&gt;</li>
* <li>Extracts the inner type T and delegates body decoding to the underlying decoder</li>
* <li>Wraps the decoded body with status code and headers into ApiResponse&lt;T&gt;</li>
* </ol>
*/
public final class ApiResponseDecoder implements Decoder {

private final Decoder delegate;

public ApiResponseDecoder(Decoder delegate) {
this.delegate = delegate;
}

@Override
public Object decode(Response response, Type type) throws IOException {
if (isApiResponseType(type)) {
Type innerType = getApiResponseInnerType(type);
Object body = delegate.decode(response, innerType);
return new ApiResponse<>(response.status(), response.headers(), body);
}
return delegate.decode(response, type);
}

private boolean isApiResponseType(Type type) {
if (type instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) type;
Type rawType = paramType.getRawType();
return rawType == ApiResponse.class;
}
return type == ApiResponse.class;
}

private Type getApiResponseInnerType(Type type) {
if (type instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) type;
Type[] typeArgs = paramType.getActualTypeArguments();
if (typeArgs.length > 0) {
return typeArgs[0];
}
}
return Object.class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,9 @@
*/
package org.apache.fineract.client.feign.util;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.FeignException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.feign.FeignException;

/**
* Exception thrown by {@link FeignCalls} utility when Feign calls fail.
Expand All @@ -49,7 +45,7 @@ private static String createMessage(FeignException e) {
sb.append(", request=").append(e.request().url());
}

String contentString = e.contentUTF8();
String contentString = e.responseBodyAsString();
if (contentString != null && !contentString.isEmpty()) {
sb.append(", errorBody=").append(contentString);
}
Expand All @@ -58,34 +54,9 @@ private static String createMessage(FeignException e) {
}

private static String extractDeveloperMessage(FeignException e) {
try {
byte[] content = e.content();
if (content == null || content.length == 0) {
return e.getMessage();
}

String contentString = new String(content, StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(contentString);

if (root.has("developerMessage")) {
return root.get("developerMessage").asText();
}

if (root.has("errors")) {
JsonNode errors = root.get("errors");
if (errors.isArray() && errors.size() > 0) {
JsonNode firstError = errors.get(0);
if (firstError.has("developerMessage")) {
return firstError.get("developerMessage").asText();
}
}
}

return contentString;
} catch (IOException ex) {
log.warn("Failed to extract developer message from error response", ex);
return e.getMessage();
if (e.getDeveloperMessage() != null) {
return e.getDeveloperMessage();
}
return e.getMessage();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
*/
package org.apache.fineract.client.feign.util;

import feign.FeignException;
import java.util.function.Supplier;
import org.apache.fineract.client.feign.FeignException;

/**
* Extension methods for Feign calls. This class is recommended to be statically imported.
Expand Down Expand Up @@ -77,4 +77,41 @@ public static void executeVoid(Runnable feignCall) throws CallFailedRuntimeExcep
public static <T> T execute(Supplier<T> feignCall) throws FeignException {
return feignCall.get();
}

/**
* Execute a Feign call expecting failure (for negative tests). Returns the exception with error details.
*
* @param feignCall
* the Feign call to execute
* @return CallFailedRuntimeException containing status code and error message
* @throws AssertionError
* if the call succeeds when failure was expected
*/
public static CallFailedRuntimeException fail(Supplier<?> feignCall) {
try {
Object result = feignCall.get();
throw new AssertionError("Expected call to fail, but it succeeded with result: " + result);
} catch (FeignException e) {
return new CallFailedRuntimeException(e);
}
}

/**
* Execute a Feign call expecting failure with void return (for negative tests). Returns the exception with error
* details.
*
* @param feignCall
* the Feign call to execute
* @return CallFailedRuntimeException containing status code and error message
* @throws AssertionError
* if the call succeeds when failure was expected
*/
public static CallFailedRuntimeException failVoid(Runnable feignCall) {
try {
feignCall.run();
throw new AssertionError("Expected call to fail, but it succeeded");
} catch (FeignException e) {
return new CallFailedRuntimeException(e);
}
}
}
Loading
Loading