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

Alternative ResponseInterceptor attempt #1634

Merged
merged 9 commits into from
Jun 23, 2022
3 changes: 2 additions & 1 deletion core/src/main/java/feign/AsyncFeign.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public AsyncFeign<C> build() {
decoder,
errorDecoder,
dismiss404,
closeAfterDecode),
closeAfterDecode, responseInterceptor),
AsyncResponseHandler.class,
capabilities);

Expand All @@ -126,6 +126,7 @@ public AsyncFeign<C> build() {
.queryMapEncoder(queryMapEncoder)
.options(options)
.requestInterceptors(requestInterceptors)
.responseInterceptor(responseInterceptor)
.invocationHandlerFactory(invocationHandlerFactory)
.build(), defaultContextSupplier, activeContextHolder);
}
Expand Down
20 changes: 8 additions & 12 deletions core/src/main/java/feign/AsyncResponseHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@

import static feign.FeignException.errorReading;
import static feign.Util.ensureClosed;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.concurrent.CompletableFuture;
import feign.Logger.Level;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.concurrent.CompletableFuture;

/**
* The response handler that is used to provide asynchronous support on top of standard response
Expand All @@ -40,15 +39,18 @@ class AsyncResponseHandler {
private final boolean dismiss404;
private final boolean closeAfterDecode;

private final ResponseInterceptor responseInterceptor;

AsyncResponseHandler(Level logLevel, Logger logger, Decoder decoder, ErrorDecoder errorDecoder,
boolean dismiss404, boolean closeAfterDecode) {
boolean dismiss404, boolean closeAfterDecode, ResponseInterceptor responseInterceptor) {
super();
this.logLevel = logLevel;
this.logger = logger;
this.decoder = decoder;
this.errorDecoder = errorDecoder;
this.dismiss404 = dismiss404;
this.closeAfterDecode = closeAfterDecode;
this.responseInterceptor = responseInterceptor;
}

boolean isVoidType(Type returnType) {
Expand Down Expand Up @@ -111,12 +113,6 @@ void handleResponse(CompletableFuture<Object> resultFuture,
}

Object decode(Response response, Type type) throws IOException {
try {
return decoder.decode(response, type);
} catch (final FeignException e) {
throw e;
} catch (final RuntimeException e) {
throw new DecodeException(response.status(), e.getMessage(), response.request(), e);
}
return responseInterceptor.aroundDecode(new InvocationContext(decoder, type, response));
}
}
10 changes: 10 additions & 0 deletions core/src/main/java/feign/BaseBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public abstract class BaseBuilder<B extends BaseBuilder<B>> {

protected final List<RequestInterceptor> requestInterceptors =
new ArrayList<>();
protected ResponseInterceptor responseInterceptor = ResponseInterceptor.DEFAULT;
protected Logger.Level logLevel = Logger.Level.NONE;
protected Contract contract = new Contract.Default();
protected Retryer retryer = new Retryer.Default();
Expand Down Expand Up @@ -196,6 +197,15 @@ public B requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
return thisB;
}

/**
* Adds a single response interceptor to the builder.
*/
public B responseInterceptor(ResponseInterceptor responseInterceptor) {
this.responseInterceptor = responseInterceptor;
return thisB;
}


/**
* Allows you to override how reflective dispatch works inside of Feign.
*/
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/feign/Capability.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ default RequestInterceptor enrich(RequestInterceptor requestInterceptor) {
return requestInterceptor;
}

default ResponseInterceptor enrich(ResponseInterceptor responseInterceptor) {
return responseInterceptor;
}

default Logger enrich(Logger logger) {
return logger;
}
Expand Down
6 changes: 4 additions & 2 deletions core/src/main/java/feign/Feign.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public static class Builder extends BaseBuilder<Builder> {

public Builder client(Client client) {
this.client = client;

return this;
}

Expand All @@ -119,8 +120,9 @@ public Feign build() {
super.enrich();

SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, dismiss404, closeAfterDecode, propagationPolicy, forceDecoding);
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors,
responseInterceptor, logger, logLevel, dismiss404, closeAfterDecode,
propagationPolicy, forceDecoding);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
errorDecoder, synchronousMethodHandlerFactory);
Expand Down
58 changes: 58 additions & 0 deletions core/src/main/java/feign/InvocationContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2012-2022 The Feign Authors
*
* 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 feign;

import static feign.FeignException.errorReading;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import java.io.IOException;
import java.lang.reflect.Type;

public class InvocationContext {

private final Decoder decoder;
private final Type returnType;
private final Response response;

InvocationContext(Decoder decoder, Type returnType, Response response) {
this.decoder = decoder;
this.returnType = returnType;
this.response = response;
}

public Object proceed() {
try {
return decoder.decode(response, returnType);
} catch (final FeignException e) {
throw e;
} catch (final RuntimeException e) {
throw new DecodeException(response.status(), e.getMessage(), response.request(), e);
} catch (IOException e) {
throw errorReading(response.request(), response, e);
}
}

public Decoder decoder() {
return decoder;
}

public Type returnType() {
return returnType;
}

public Response response() {
return response;
}

}
38 changes: 38 additions & 0 deletions core/src/main/java/feign/ResponseInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2012-2022 The Feign Authors
*
* 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 feign;

import java.io.IOException;
import java.util.function.Function;

/**
* Zero or One {@code ResponseInterceptor} may be configured for purposes such as verify or modify
* headers of response, verify the business status of decoded object. Once interceptors are applied,
* {@link ResponseInterceptor#aroundDecode(Response, Function)} is called around decode method
* called
*/
public interface ResponseInterceptor {

ResponseInterceptor DEFAULT = InvocationContext::proceed;

/**
* Called for response around decode, must either manually invoke
* {@link InvocationContext#proceed} or manually create a new response object
*
* @param invocationContext information surrounding the response been decoded
* @return decoded response
*/
Object aroundDecode(InvocationContext invocationContext) throws IOException;

}
40 changes: 22 additions & 18 deletions core/src/main/java/feign/SynchronousMethodHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@
*/
package feign;

import static feign.ExceptionPropagationPolicy.UNWRAP;
import static feign.FeignException.errorExecuting;
import static feign.Util.checkNotNull;
import feign.InvocationHandlerFactory.MethodHandler;
import feign.Request.Options;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import feign.InvocationHandlerFactory.MethodHandler;
import feign.Request.Options;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;
import static feign.ExceptionPropagationPolicy.UNWRAP;
import static feign.FeignException.errorExecuting;
import static feign.Util.checkNotNull;

final class SynchronousMethodHandler implements MethodHandler {

Expand All @@ -36,6 +36,7 @@ final class SynchronousMethodHandler implements MethodHandler {
private final Client client;
private final Retryer retryer;
private final List<RequestInterceptor> requestInterceptors;
private final ResponseInterceptor responseInterceptor;
private final Logger logger;
private final Logger.Level logLevel;
private final RequestTemplate.Factory buildTemplateFromArgs;
Expand All @@ -48,8 +49,8 @@ final class SynchronousMethodHandler implements MethodHandler {


private SynchronousMethodHandler(Target<?> target, Client client, Retryer retryer,
List<RequestInterceptor> requestInterceptors, Logger logger,
Logger.Level logLevel, MethodMetadata metadata,
List<RequestInterceptor> requestInterceptors, ResponseInterceptor responseInterceptor,
Logger logger, Logger.Level logLevel, MethodMetadata metadata,
RequestTemplate.Factory buildTemplateFromArgs, Options options,
Decoder decoder, ErrorDecoder errorDecoder, boolean dismiss404,
boolean closeAfterDecode, ExceptionPropagationPolicy propagationPolicy,
Expand All @@ -66,6 +67,7 @@ private SynchronousMethodHandler(Target<?> target, Client client, Retryer retrye
this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target);
this.options = checkNotNull(options, "options for %s", target);
this.propagationPolicy = propagationPolicy;
this.responseInterceptor = responseInterceptor;

if (forceDecoding) {
// internal only: usual handling will be short-circuited, and all responses will be passed to
Expand All @@ -75,7 +77,7 @@ private SynchronousMethodHandler(Target<?> target, Client client, Retryer retrye
} else {
this.decoder = null;
this.asyncResponseHandler = new AsyncResponseHandler(logLevel, logger, decoder, errorDecoder,
dismiss404, closeAfterDecode);
dismiss404, closeAfterDecode, responseInterceptor);
}
}

Expand Down Expand Up @@ -130,19 +132,18 @@ Object executeAndDecode(RequestTemplate template, Options options) throws Throwa
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);


if (decoder != null)
return decoder.decode(response, metadata.returnType());
if (decoder != null) {
return responseInterceptor
.aroundDecode(new InvocationContext(decoder, metadata.returnType(), response));
}

CompletableFuture<Object> resultFuture = new CompletableFuture<>();
asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
metadata.returnType(),
elapsedTime);
metadata.returnType(), elapsedTime);

try {
if (!resultFuture.isDone())
throw new IllegalStateException("Response handling not done");

return resultFuture.join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
Expand Down Expand Up @@ -179,6 +180,7 @@ static class Factory {
private final Client client;
private final Retryer retryer;
private final List<RequestInterceptor> requestInterceptors;
private final ResponseInterceptor responseInterceptor;
private final Logger logger;
private final Logger.Level logLevel;
private final boolean dismiss404;
Expand All @@ -187,11 +189,13 @@ static class Factory {
private final boolean forceDecoding;

Factory(Client client, Retryer retryer, List<RequestInterceptor> requestInterceptors,
ResponseInterceptor responseInterceptor,
Logger logger, Logger.Level logLevel, boolean dismiss404, boolean closeAfterDecode,
ExceptionPropagationPolicy propagationPolicy, boolean forceDecoding) {
this.client = checkNotNull(client, "client");
this.retryer = checkNotNull(retryer, "retryer");
this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
this.responseInterceptor = responseInterceptor;
this.logger = checkNotNull(logger, "logger");
this.logLevel = checkNotNull(logLevel, "logLevel");
this.dismiss404 = dismiss404;
Expand All @@ -206,8 +210,8 @@ public MethodHandler create(Target<?> target,
Options options,
Decoder decoder,
ErrorDecoder errorDecoder) {
return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
logLevel, md, buildTemplateFromArgs, options, decoder,
return new SynchronousMethodHandler(target, client, retryer, requestInterceptors,
responseInterceptor, logger, logLevel, md, buildTemplateFromArgs, options, decoder,
errorDecoder, dismiss404, closeAfterDecode, propagationPolicy, forceDecoding);
}
}
Expand Down