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 20 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
8 changes: 8 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@
</execution>
</executions>
</plugin>
<plugin>
iain-henderson marked this conversation as resolved.
Show resolved Hide resolved
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>

Expand Down
125 changes: 125 additions & 0 deletions core/src/main/java/feign/InvocationContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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 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 ErrorDecoder errorDecoder;
private final boolean dismiss404;
private final boolean closeAfterDecode;
private final boolean decodeVoid;
private final Response response;
private final Type returnType;

InvocationContext(String configKey, Decoder decoder, ErrorDecoder errorDecoder,
boolean dismiss404, boolean closeAfterDecode, boolean decodeVoid, Response response,
Type returnType) {
this.configKey = configKey;
this.decoder = decoder;
this.errorDecoder = errorDecoder;
this.dismiss404 = dismiss404;
this.closeAfterDecode = closeAfterDecode;
this.decodeVoid = decodeVoid;
this.response = response;
this.returnType = returnType;
}

public Decoder decoder() {
return decoder;
}

public Type returnType() {
return returnType;
}

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");
}
}
3 changes: 2 additions & 1 deletion core/src/main/java/feign/Logger.java
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ protected static String resolveProtocolVersion(Request.ProtocolVersion protocolV
/**
* Controls the level of logging.
*/
public enum Level {
public enum Level
{
/**
* No logging.
*/
Expand Down
58 changes: 58 additions & 0 deletions core/src/main/java/feign/RedirectionInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.io.IOException;
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 intercept(InvocationContext invocationContext, Chain chain) 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 chain.next(invocationContext);
} 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;
}
}
28 changes: 15 additions & 13 deletions core/src/main/java/feign/Request.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,41 +31,43 @@
*/
public final class Request implements Serializable {

public enum HttpMethod {
public enum HttpMethod
{
GET, HEAD, POST(true), PUT(true), DELETE, CONNECT, OPTIONS, TRACE, PATCH(true);

private final boolean withBody;
private final boolean withBody;

HttpMethod() {
HttpMethod() {
this(false);
}

HttpMethod(boolean withBody) {
HttpMethod(boolean withBody) {
this.withBody = withBody;
}

public boolean isWithBody() {
return this.withBody;
}
}
public boolean isWithBody() {
return this.withBody;
}}

public enum ProtocolVersion{

public enum ProtocolVersion {
HTTP_1_0("HTTP/1.0"), HTTP_1_1("HTTP/1.1"), HTTP_2("HTTP/2.0"), MOCK;
HTTP_1_0("HTTP/1.0"), HTTP_1_1("HTTP/1.1"), HTTP_2("HTTP/2.0"), MOCK;

final String protocolVersion;

ProtocolVersion() {
ProtocolVersion() {
protocolVersion = name();
}

ProtocolVersion(String protocolVersion) {
ProtocolVersion(String protocolVersion) {
this.protocolVersion = protocolVersion;
}

@Override
@Override
public String toString() {
return protocolVersion;
}

}

/**
Expand Down
69 changes: 6 additions & 63 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 Down Expand Up @@ -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 executionChain.next(
new InvocationContext(configKey, decoder, errorDecoder, dismiss404, closeAfterDecode,
decodeVoid, response, returnType));
} 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 = executionChain.next(
new ResponseInterceptor.Context(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());
}
}
}
Loading