diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index 4f38a0b96..f63b796a5 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -98,6 +98,7 @@ public static class Builder { private final List requestInterceptors = new ArrayList(); + private ResponseInterceptor responseInterceptor = null; private Logger.Level logLevel = Logger.Level.NONE; private Contract contract = new Contract.Default(); private Client client = new Client.Default(null, null); @@ -227,6 +228,14 @@ public Builder requestInterceptor(RequestInterceptor requestInterceptor) { return this; } + /** + * Adds a single response interceptor to the builder. + */ + public Builder responseInterceptor(ResponseInterceptor responseInterceptor) { + this.responseInterceptor = responseInterceptor; + return this; + } + /** * Sets the full set of request interceptors for the builder, overwriting any previous * interceptors. @@ -305,10 +314,15 @@ public Feign build() { InvocationHandlerFactory invocationHandlerFactory = Capability.enrich(this.invocationHandlerFactory, capabilities); QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities); + ResponseInterceptor responseInterceptor = null; + if (this.responseInterceptor != null) { + responseInterceptor = Capability.enrich(this.responseInterceptor, capabilities); + } 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); diff --git a/core/src/main/java/feign/ResponseInterceptor.java b/core/src/main/java/feign/ResponseInterceptor.java new file mode 100644 index 000000000..75ce6e0f9 --- /dev/null +++ b/core/src/main/java/feign/ResponseInterceptor.java @@ -0,0 +1,36 @@ +/* + * 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.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 { + + /** + * Called for response around decode, user can use supplied {@link Function} decoder function to + * decode response data. + * + * @param response + * @param decoder + * @return + */ + void aroundDecode(Response response, Function decoder); + +} diff --git a/core/src/main/java/feign/SynchronousMethodHandler.java b/core/src/main/java/feign/SynchronousMethodHandler.java index 1d8f39a0d..713db33b0 100644 --- a/core/src/main/java/feign/SynchronousMethodHandler.java +++ b/core/src/main/java/feign/SynchronousMethodHandler.java @@ -13,6 +13,9 @@ */ package feign; +import static feign.ExceptionPropagationPolicy.UNWRAP; +import static feign.FeignException.errorExecuting; +import static feign.Util.checkNotNull; import java.io.IOException; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -21,11 +24,9 @@ import java.util.stream.Stream; import feign.InvocationHandlerFactory.MethodHandler; import feign.Request.Options; +import feign.codec.DecodeException; 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 { @@ -36,6 +37,7 @@ final class SynchronousMethodHandler implements MethodHandler { private final Client client; private final Retryer retryer; private final List requestInterceptors; + private final ResponseInterceptor responseInterceptor; private final Logger logger; private final Logger.Level logLevel; private final RequestTemplate.Factory buildTemplateFromArgs; @@ -48,8 +50,8 @@ final class SynchronousMethodHandler implements MethodHandler { private SynchronousMethodHandler(Target target, Client client, Retryer retryer, - List requestInterceptors, Logger logger, - Logger.Level logLevel, MethodMetadata metadata, + List 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, @@ -66,6 +68,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 @@ -130,9 +133,20 @@ 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) { + if (responseInterceptor != null) { + responseInterceptor.aroundDecode(response, (res) -> { + try { + return decoder.decode(res, metadata.returnType()); + } catch (IOException e) { + throw new DecodeException(res.status(), "decode error cause of io exception", + res.request(), e); + } + }); + } else { + return decoder.decode(response, metadata.returnType()); + } + } CompletableFuture resultFuture = new CompletableFuture<>(); asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response, @@ -142,7 +156,6 @@ Object executeAndDecode(RequestTemplate template, Options options) throws Throwa try { if (!resultFuture.isDone()) throw new IllegalStateException("Response handling not done"); - return resultFuture.join(); } catch (CompletionException e) { Throwable cause = e.getCause(); @@ -179,6 +192,7 @@ static class Factory { private final Client client; private final Retryer retryer; private final List requestInterceptors; + private final ResponseInterceptor responseInterceptor; private final Logger logger; private final Logger.Level logLevel; private final boolean dismiss404; @@ -187,11 +201,13 @@ static class Factory { private final boolean forceDecoding; Factory(Client client, Retryer retryer, List 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; @@ -206,8 +222,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); } }