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

Feature/decoder interceptor to response interceptor #2116

Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0764d19
Refactor so that ResponseInterceptor intercepts the response (in the …
iain-henderson Jun 29, 2023
2892824
Merge branch 'OpenFeign:master' into feature/decoder-interceptor-to-r…
iain-henderson Jul 3, 2023
9c233f0
Merge branch 'master' into feature/decoder-interceptor-to-response-in…
velo Jul 5, 2023
b549111
Merge branch 'master' into feature/decoder-interceptor-to-response-in…
iain-henderson Jul 21, 2023
d6c399d
Merge branch 'master' into feature/decoder-interceptor-to-response-in…
iain-henderson Jul 26, 2023
dcf876f
Merge branch 'master' into feature/decoder-interceptor-to-response-in…
iain-henderson Aug 6, 2023
47e25c0
Add a default RedirectionInterceptor as an implementation of Response…
iain-henderson Aug 6, 2023
af08d1c
Merge branch 'master' into feature/decoder-interceptor-to-response-in…
iain-henderson Aug 6, 2023
3b20b59
Update README to include ResponseInterceptor
iain-henderson Aug 6, 2023
1c45e7d
Add copyright notice to RedirectionInterceptor
iain-henderson Aug 9, 2023
c5b51c4
Correct formatting using maven
iain-henderson Aug 9, 2023
e807dad
Merge commit 'b46ad267525d9aa4b130d9034a217a9fbda46072' into feature/…
iain-henderson Aug 9, 2023
4315cc1
Merge commit 'c65915b0b560c2b3dcf14b2e8f644b27d10a3deb' into feature/…
iain-henderson Aug 14, 2023
2da31ac
Updates in response to CodeRabbit
iain-henderson Aug 28, 2023
5275235
Merge commit '78dd615a68978bffe0d57f3869241a26bd2486b7' into feature/…
iain-henderson Aug 28, 2023
c1fc84c
more CodeRabbitAI suggestions
iain-henderson Aug 28, 2023
bdb890b
Merge branch 'master' into feature/decoder-interceptor-to-response-in…
velo Aug 28, 2023
ec675b9
Merge commit '2c00066d4a7a1f1882708166f8b2cbaabe721efa' into feature/…
iain-henderson Aug 29, 2023
dc1e3b7
Add unit tests for chained ResponseInterceptor instances
iain-henderson Aug 29, 2023
6fa78d7
fixing formatting
iain-henderson Aug 29, 2023
e5b2308
formatting and responding to CodeRabbitAI comment
iain-henderson Aug 29, 2023
ee83bc7
Reverting Feign-core pom
iain-henderson Aug 30, 2023
008fc77
Cleanup Javadocs in ResponseInterceptor and RedirectionInterceptor
iain-henderson Aug 31, 2023
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,28 @@ created for each `Client` execution, allowing you to maintain state bewteen each
If the retry is determined to be unsuccessful, the last `RetryException` will be thrown. To throw the original
cause that led to the unsuccessful retry, build your Feign client with the `exceptionPropagationPolicy()` option.

#### Response Interceptor
If you need to treat what would otherwise be an error as a success and return a result rather than throw an exception then you may use a `ResponseInterceptor`.

As an example Feign includes a simple `RedirectionInterceptor` that can be used to extract the location header from redirection responses.
```java
public interface Api {
// returns a 302 response
@RequestLine("GET /location")
String location();
}

public class MyApp {
public static void main(String[] args) {
// Configure the HTTP client to ignore redirection
Api api = Feign.builder()
.options(new Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, false))
.responseInterceptor(new RedirectionInterceptor())
.target(Api.class, "https://redirect.example.com");
}
}
```

### Metrics
By default, feign won't collect any metrics.

Expand Down
99 changes: 83 additions & 16 deletions core/src/main/java/feign/InvocationContext.java
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,35 @@
package feign;

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

public class InvocationContext {

private static final long MAX_RESPONSE_BUFFER_SIZE = 8192L;
private final String configKey;
private final Decoder decoder;
private final Type returnType;
private final ErrorDecoder errorDecoder;
private final boolean dismiss404;
private final boolean closeAfterDecode;
private final boolean decodeVoid;
private final Response response;
private final Type returnType;

InvocationContext(Decoder decoder, Type returnType, Response response) {
InvocationContext(String configKey, Decoder decoder, ErrorDecoder errorDecoder,
boolean dismiss404, boolean closeAfterDecode, boolean decodeVoid, Response response,
Type returnType) {
this.configKey = configKey;
this.decoder = decoder;
this.returnType = returnType;
this.errorDecoder = errorDecoder;
this.dismiss404 = dismiss404;
this.closeAfterDecode = closeAfterDecode;
this.decodeVoid = decodeVoid;
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);
}
this.returnType = returnType;
}

public Decoder decoder() {
Expand All @@ -55,4 +57,69 @@ public Response response() {
return response;
}

public Object proceed() throws Exception {
if (returnType == Response.class) {
return disconnectResponseBodyIfNeeded(response);
}

try {
final boolean shouldDecodeResponseBody =
(response.status() >= 200 && response.status() < 300)
|| (response.status() == 404 && dismiss404
&& !isVoidType(returnType));

if (!shouldDecodeResponseBody) {
throw decodeError(configKey, response);
}

if (isVoidType(returnType) && !decodeVoid) {
ensureClosed(response.body());
return null;
}

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);
}
} finally {
if (closeAfterDecode) {
ensureClosed(response.body());
}
}
}
iain-henderson marked this conversation as resolved.
Show resolved Hide resolved

private static Response disconnectResponseBodyIfNeeded(Response response) throws IOException {
final boolean shouldDisconnectResponseBody = response.body() != null
&& response.body().length() != null
&& response.body().length() <= MAX_RESPONSE_BUFFER_SIZE;
if (!shouldDisconnectResponseBody) {
return response;
}

try {
final byte[] bodyData = Util.toByteArray(response.body().asInputStream());
return response.toBuilder().body(bodyData).build();
} finally {
ensureClosed(response.body());
}
iain-henderson marked this conversation as resolved.
Show resolved Hide resolved
}

private Exception decodeError(String methodKey, Response response) {
try {
return errorDecoder.decode(methodKey, response);
} finally {
ensureClosed(response.body());
}
iain-henderson marked this conversation as resolved.
Show resolved Hide resolved
}

private boolean isVoidType(Type returnType) {
return returnType == Void.class
|| returnType == void.class
|| returnType.getTypeName().equals("kotlin.Unit");
}
}
57 changes: 57 additions & 0 deletions core/src/main/java/feign/RedirectionInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2012-2023 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.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;

/**
* An implementation of {@link ResponseInterceptor} the returns the value of the location header
* when appropriate.
*
* This implementation will return Collections, Strings, types that can be constructed from those
*/
public class RedirectionInterceptor implements ResponseInterceptor {
@Override
public Object aroundDecode(InvocationContext invocationContext) throws Exception {
Response response = invocationContext.response();
int status = response.status();
Object returnValue = null;
if (300 <= status && status < 400 && response.headers().containsKey("Location")) {
Type returnType = rawType(invocationContext.returnType());
Collection<String> locations = response.headers().get("Location");
if (Collection.class.equals(returnType)) {
returnValue = locations;
} else if (String.class.equals(returnType)) {
if (locations.isEmpty()) {
returnValue = "";
} else {
returnValue = locations.stream().findFirst().orElse("");
}
}
}
if (returnValue == null) {
return invocationContext.proceed();
} else {
response.close();
iain-henderson marked this conversation as resolved.
Show resolved Hide resolved
return returnValue;
}
iain-henderson marked this conversation as resolved.
Show resolved Hide resolved
iain-henderson marked this conversation as resolved.
Show resolved Hide resolved
}

private Type rawType(Type type) {
return type instanceof ParameterizedType ? ((ParameterizedType) type).getRawType() : type;
}
}
71 changes: 7 additions & 64 deletions core/src/main/java/feign/ResponseHandler.java
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
*/
public class ResponseHandler {

private static final long MAX_RESPONSE_BUFFER_SIZE = 8192L;

private final Level logLevel;
private final Logger logger;

Expand All @@ -51,8 +49,8 @@ public ResponseHandler(Level logLevel, Logger logger, Decoder decoder,
this.errorDecoder = errorDecoder;
this.dismiss404 = dismiss404;
this.closeAfterDecode = closeAfterDecode;
this.responseInterceptor = responseInterceptor;
this.decodeVoid = decodeVoid;
this.responseInterceptor = responseInterceptor;
}

public Object handleResponse(String configKey,
Expand All @@ -62,32 +60,20 @@ public Object handleResponse(String configKey,
throws Exception {
try {
response = logAndRebufferResponseIfNeeded(configKey, response, elapsedTime);
if (returnType == Response.class) {
return disconnectResponseBodyIfNeeded(response);
}

final boolean shouldDecodeResponseBody = (response.status() >= 200 && response.status() < 300)
|| (response.status() == 404 && dismiss404 && !isVoidType(returnType));

if (!shouldDecodeResponseBody) {
throw decodeError(configKey, response);
}

return decode(response, returnType);
return responseInterceptor.aroundDecode(
new InvocationContext(configKey, decoder, errorDecoder, dismiss404, closeAfterDecode,
decodeVoid, response, returnType));
iain-henderson marked this conversation as resolved.
Show resolved Hide resolved
} catch (final IOException e) {
if (logLevel != Level.NONE) {
logger.logIOException(configKey, logLevel, e, elapsedTime);
}
throw errorReading(response.request(), response, e);
} catch (Exception e) {
ensureClosed(response.body());
throw e;
}
}

private boolean isVoidType(Type returnType) {
return returnType == Void.class
|| returnType == void.class
|| returnType.getTypeName().equals("kotlin.Unit");
}

private Response logAndRebufferResponseIfNeeded(String configKey,
Response response,
long elapsedTime)
Expand All @@ -98,47 +84,4 @@ private Response logAndRebufferResponseIfNeeded(String configKey,

return logger.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);
}

private static Response disconnectResponseBodyIfNeeded(Response response) throws IOException {
final boolean shouldDisconnectResponseBody = response.body() != null
&& response.body().length() != null
&& response.body().length() <= MAX_RESPONSE_BUFFER_SIZE;
if (!shouldDisconnectResponseBody) {
return response;
}

try {
final byte[] bodyData = Util.toByteArray(response.body().asInputStream());
return response.toBuilder().body(bodyData).build();
} finally {
ensureClosed(response.body());
}
}

private Object decode(Response response, Type type) throws IOException {
if (isVoidType(type) && !decodeVoid) {
ensureClosed(response.body());
return null;
}

try {
final Object result = responseInterceptor.aroundDecode(
new InvocationContext(decoder, type, response));
if (closeAfterDecode) {
ensureClosed(response.body());
}
return result;
} catch (Exception e) {
ensureClosed(response.body());
throw e;
}
}

private Exception decodeError(String methodKey, Response response) {
try {
return errorDecoder.decode(methodKey, response);
} finally {
ensureClosed(response.body());
}
}
}
19 changes: 8 additions & 11 deletions core/src/main/java/feign/ResponseInterceptor.java
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,23 @@
*/
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
* headers of response, verify the business status of decoded object. Once the interceptor is
* applied, {@link ResponseInterceptor#aroundDecode(InvocationContext)} 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
* Called by {@link ResponseHandler} after refreshing the response and wrapped around the whole
* decode process, must either manually invoke {@link InvocationContext#proceed} or manually
* create a new response object
*
* @param invocationContext information surrounding the response been decoded
* @param invocationContext information surrounding the response being decoded
* @return decoded response
*/
iain-henderson marked this conversation as resolved.
Show resolved Hide resolved
Object aroundDecode(InvocationContext invocationContext) throws IOException;
iain-henderson marked this conversation as resolved.
Show resolved Hide resolved

Object aroundDecode(InvocationContext invocationContext) throws Exception;
}
Loading