Skip to content

Commit

Permalink
Add typed response (#2206)
Browse files Browse the repository at this point in the history
* Add typed response

* Update code as per suggestions

---------

Co-authored-by: Marvin Froeder <velo@users.noreply.github.com>
  • Loading branch information
gromspys and velo authored Oct 27, 2023
1 parent 87179ca commit e37d7d5
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 9 deletions.
26 changes: 18 additions & 8 deletions core/src/main/java/feign/InvocationContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,13 @@ public Object proceed() throws Exception {
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);
Class<?> rawType = Types.getRawType(returnType);
if (TypedResponse.class.isAssignableFrom(rawType)) {
Type bodyType = Types.resolveLastTypeParameter(returnType, TypedResponse.class);
return TypedResponse.builder(response).body(decode(response, bodyType)).build();
}

return decode(response, returnType);
} finally {
if (closeAfterDecode) {
ensureClosed(response.body());
Expand All @@ -116,6 +114,18 @@ private static Response disconnectResponseBodyIfNeeded(Response response) throws
}
}

private Object decode(Response response, Type returnType) {
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);
}
}

private Exception decodeError(String methodKey, Response response) {
try {
return errorDecoder.decode(methodKey, response);
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/feign/Response.java
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ public Charset charset() {

@Override
public String toString() {
StringBuilder builder = new StringBuilder("HTTP/1.1 ").append(status);
StringBuilder builder =
new StringBuilder(protocolVersion.toString()).append(" ").append(status);
if (reason != null) builder.append(' ').append(reason);
builder.append('\n');
for (String field : headers.keySet()) {
Expand Down
175 changes: 175 additions & 0 deletions core/src/main/java/feign/TypedResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* 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.Util.*;

import feign.Request.ProtocolVersion;
import java.util.Collection;
import java.util.Map;

public final class TypedResponse<T> {

private final int status;
private final String reason;
private final Map<String, Collection<String>> headers;
private final T body;
private final Request request;
private final ProtocolVersion protocolVersion;

private TypedResponse(Builder<T> builder) {
checkState(builder.request != null, "original request is required");
this.status = builder.status;
this.request = builder.request;
this.reason = builder.reason; // nullable
this.headers = caseInsensitiveCopyOf(builder.headers);
this.body = builder.body;
this.protocolVersion = builder.protocolVersion;
}

public static <T> Builder<T> builder() {
return new Builder<T>();
}

public static <T> Builder<T> builder(Response source) {
return new Builder<T>(source);
}

public static final class Builder<T> {
int status;
String reason;
Map<String, Collection<String>> headers;
T body;
Request request;
private ProtocolVersion protocolVersion = ProtocolVersion.HTTP_1_1;

Builder() {}

Builder(Response source) {
this.status = source.status();
this.reason = source.reason();
this.headers = source.headers();
this.request = source.request();
this.protocolVersion = source.protocolVersion();
}

/**
* @see TypedResponse#status
*/
public Builder status(int status) {
this.status = status;
return this;
}

/**
* @see TypedResponse#reason
*/
public Builder reason(String reason) {
this.reason = reason;
return this;
}

/**
* @see TypedResponse#headers
*/
public Builder headers(Map<String, Collection<String>> headers) {
this.headers = headers;
return this;
}

/**
* @see TypedResponse#body
*/
public Builder body(T body) {
this.body = body;
return this;
}

/**
* @see TypedResponse#request
*/
public Builder request(Request request) {
checkNotNull(request, "request is required");
this.request = request;
return this;
}

/** HTTP protocol version */
public Builder protocolVersion(ProtocolVersion protocolVersion) {
this.protocolVersion = protocolVersion;
return this;
}

public TypedResponse build() {
return new TypedResponse<T>(this);
}
}

/**
* status code. ex {@code 200}
*
* <p>See <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html" >rfc2616</a>
*/
public int status() {
return status;
}

/**
* Nullable and not set when using http/2
*
* <p>See https://github.com/http2/http2-spec/issues/202
*/
public String reason() {
return reason;
}

/** Returns a case-insensitive mapping of header names to their values. */
public Map<String, Collection<String>> headers() {
return headers;
}

/** if present, the response had a body */
public T body() {
return body;
}

/** the request that generated this response */
public Request request() {
return request;
}

/**
* the HTTP protocol version
*
* @return HTTP protocol version or empty if a client does not provide it
*/
public ProtocolVersion protocolVersion() {
return protocolVersion;
}

@Override
public String toString() {
StringBuilder builder =
new StringBuilder(protocolVersion.toString()).append(" ").append(status);
if (reason != null) builder.append(' ').append(reason);
builder.append('\n');
for (String field : headers.keySet()) {
for (String value : valuesOrEmpty(headers, field)) {
builder.append(field).append(": ").append(value).append('\n');
}
}
if (body != null) builder.append('\n').append(body);
return builder.toString();
}
}
19 changes: 19 additions & 0 deletions core/src/test/java/feign/FeignTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import static feign.Util.UTF_8;
import static feign.assertj.MockWebServerAssertions.assertThat;
import static java.util.Collections.emptyList;
import static junit.framework.TestCase.assertNotNull;
import static org.assertj.core.data.MapEntry.entry;
import static org.hamcrest.CoreMatchers.isA;
import static org.junit.Assert.assertEquals;
Expand Down Expand Up @@ -85,6 +86,21 @@ public void arrayQueryMapParams() throws Exception {
assertThat(server.takeRequest()).hasPath("/?1=apple&1=pear");
}

@Test
public void typedResponse() throws Exception {
server.enqueue(new MockResponse().setBody("foo"));

TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());

TypedResponse response = api.getWithTypedResponse();

assertEquals(200, response.status());
assertEquals("foo", response.body());
assertEquals("HTTP/1.1", response.protocolVersion().toString());
assertNotNull(response.headers());
assertNotNull(response.request());
}

@Test
public void postTemplateParamsResolve() throws Exception {
server.enqueue(new MockResponse().setBody("foo"));
Expand Down Expand Up @@ -1260,6 +1276,9 @@ void form(
@RequestLine("GET /")
Response queryMapWithArrayValues(@QueryMap Map<String, String[]> twos);

@RequestLine("GET /")
TypedResponse<String> getWithTypedResponse();

@RequestLine("POST /?clock={clock}")
void expand(@Param(value = "clock", expander = ClockToMillis.class) Clock clock);

Expand Down

0 comments on commit e37d7d5

Please sign in to comment.