Skip to content

Commit c8aad55

Browse files
dbyron-sfCalvinTse
andauthoredDec 14, 2021
fix(retrofit): handle non-message properties when deserializing response body in SpinnakerServerException (spinnaker#913)
Co-authored-by: calvintse <[email protected]>
1 parent 554c5ed commit c8aad55

File tree

2 files changed

+77
-3
lines changed

2 files changed

+77
-3
lines changed
 

‎kork-retrofit/src/main/java/com/netflix/spinnaker/kork/retrofit/exceptions/SpinnakerServerException.java

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.netflix.spinnaker.kork.retrofit.exceptions;
1818

1919
import com.fasterxml.jackson.annotation.JsonCreator;
20+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2021
import com.fasterxml.jackson.annotation.JsonProperty;
2122
import com.netflix.spinnaker.kork.annotations.NonnullByDefault;
2223
import com.netflix.spinnaker.kork.exceptions.SpinnakerException;
@@ -74,6 +75,12 @@ final String getRawMessage() {
7475
}
7576

7677
@Getter
78+
// Use JsonIgnoreProperties because some responses contain properties that
79+
// cannot be mapped to the RetrofitErrorResponseBody class. If the default
80+
// JacksonConverter (with no extra configurations) is used to deserialize the
81+
// response body and properties other than "message" exist in the JSON
82+
// response, there will be an UnrecognizedPropertyException.
83+
@JsonIgnoreProperties(ignoreUnknown = true)
7784
private static final class RetrofitErrorResponseBody {
7885
private final String message;
7986

‎kork-retrofit/src/test/java/com/netflix/spinnaker/kork/retrofit/exceptions/SpinnakerRetrofitErrorHandlerTest.java

+70-3
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,22 @@
2121
import static org.junit.jupiter.api.Assertions.assertNull;
2222
import static org.junit.jupiter.api.Assertions.assertThrows;
2323

24+
import com.fasterxml.jackson.databind.DeserializationFeature;
25+
import com.fasterxml.jackson.databind.ObjectMapper;
2426
import com.netflix.spinnaker.kork.web.exceptions.NotFoundException;
27+
import java.util.HashMap;
28+
import java.util.Map;
2529
import okhttp3.mockwebserver.MockResponse;
2630
import okhttp3.mockwebserver.MockWebServer;
2731
import org.junit.jupiter.api.AfterAll;
2832
import org.junit.jupiter.api.BeforeAll;
2933
import org.junit.jupiter.api.Test;
34+
import org.junit.jupiter.params.ParameterizedTest;
35+
import org.junit.jupiter.params.provider.ValueSource;
3036
import org.springframework.http.HttpStatus;
3137
import retrofit.RestAdapter;
3238
import retrofit.client.Response;
39+
import retrofit.converter.JacksonConverter;
3340
import retrofit.http.GET;
3441

3542
public class SpinnakerRetrofitErrorHandlerTest {
@@ -55,16 +62,76 @@ public static void shutdownOnce() throws Exception {
5562
}
5663

5764
@Test
58-
public void testNotFoundIsNotRetryable() throws Exception {
65+
public void testNotFoundIsNotRetryable() {
5966
mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.value()));
6067
NotFoundException notFoundException =
6168
assertThrows(NotFoundException.class, () -> retrofitService.getFoo());
6269
assertNotNull(notFoundException.getRetryable());
6370
assertFalse(notFoundException.getRetryable());
6471
}
6572

73+
@ParameterizedTest(name = "Deserialize response using {0}")
74+
// Test the different converters used to deserialize the response body to the
75+
// SpinnakerServerException.RetrofitErrorResponseBody class:
76+
//
77+
// - the JacksonConverter constructed without an ObjectMapper is used in
78+
// Clouddriver's RestAdapter to communicate with Front50Service
79+
//
80+
// - the JacksonConverter constructed with an ObjectMapper is used in Rosco's RestAdapter to
81+
// communicate with Clouddriver
82+
//
83+
// - GSONConverter is the default converter used by Retrofit if no converter
84+
// is set when building out the RestAdapter
85+
@ValueSource(
86+
strings = {"Default_GSONConverter", "JacksonConverter", "JacksonConverterWithObjectMapper"})
87+
public void testResponseWithExtraField(String retrofitConverter) throws Exception {
88+
Map<String, String> responseBodyMap = new HashMap<>();
89+
responseBodyMap.put("timestamp", "123123123123");
90+
responseBodyMap.put("message", "Not Found error Message");
91+
String responseBodyString = new ObjectMapper().writeValueAsString(responseBodyMap);
92+
93+
RestAdapter.Builder restAdapter =
94+
new RestAdapter.Builder()
95+
.setEndpoint(mockWebServer.url("/").toString())
96+
.setErrorHandler(SpinnakerRetrofitErrorHandler.getInstance());
97+
98+
if (retrofitConverter.equals("JacksonConverter")) {
99+
restAdapter.setConverter(new JacksonConverter());
100+
} else if (retrofitConverter.equals("JacksonConverterWithObjectMapper")) {
101+
ObjectMapper objectMapper =
102+
new ObjectMapper()
103+
.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)
104+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
105+
106+
restAdapter.setConverter(new JacksonConverter(objectMapper));
107+
}
108+
109+
RetrofitService retrofitServiceTestConverter =
110+
restAdapter.build().create(RetrofitService.class);
111+
112+
mockWebServer.enqueue(
113+
new MockResponse()
114+
.setBody(responseBodyString)
115+
// an arbitrary response code -- one that
116+
// SpinnakerRetrofitErrorHandler converts to a
117+
// SpinnakerServerException (or one of its children).
118+
.setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value()));
119+
120+
// If the converter can not deserialize the response body to the
121+
// SpinnakerServerException.RetrofitErrorResponseBody
122+
// class, then a RuntimeException will be thrown with a ConversionException nested inside.
123+
//
124+
// java.lang.RuntimeException:
125+
// retrofit.converter.ConversionException:
126+
// com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field
127+
// "..."
128+
//
129+
// so make sure we get a SpinnakerHttpException from calling getFoo
130+
assertThrows(SpinnakerHttpException.class, retrofitServiceTestConverter::getFoo);
131+
}
132+
66133
@Test
67-
public void testBadRequestIsNotRetryable() throws Exception {
134+
public void testBadRequestIsNotRetryable() {
68135
mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.BAD_REQUEST.value()));
69136
SpinnakerHttpException spinnakerHttpException =
70137
assertThrows(SpinnakerHttpException.class, () -> retrofitService.getFoo());
@@ -73,7 +140,7 @@ public void testBadRequestIsNotRetryable() throws Exception {
73140
}
74141

75142
@Test
76-
public void testOtherClientErrorHasNullRetryable() throws Exception {
143+
public void testOtherClientErrorHasNullRetryable() {
77144
// Arbitrarily choose GONE as an example of a client (e.g. 4xx) error that
78145
// we expect to have null retryable
79146
mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.GONE.value()));

0 commit comments

Comments
 (0)