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

feat(retrofit): add getErrorBodyAs() method to SpinnakerServerException #1027

Closed

Conversation

Luthan95
Copy link
Contributor

@Luthan95 Luthan95 commented Mar 20, 2023

add the getErrorBodyAs() method to SpinnakerServerException which returns the error response body as Map to handle the exception.

@Luthan95 Luthan95 force-pushed the customexception_retrofitErrorbody branch from c171653 to cbfa0ed Compare March 20, 2023 06:27
@Luthan95 Luthan95 changed the title refactor(SpinnakerServerException): add method which returns retrofit… refactor(SpinnakerServerException): create method which returns retrofit error response body as Map Mar 20, 2023
@Luthan95 Luthan95 changed the title refactor(SpinnakerServerException): create method which returns retrofit error response body as Map refactor(retrofit): add getErrorBodyAs() method to SpinnakerServerException Mar 20, 2023
@Luthan95 Luthan95 force-pushed the customexception_retrofitErrorbody branch from e497f0b to f610a6b Compare April 28, 2023 07:27
@Luthan95 Luthan95 force-pushed the customexception_retrofitErrorbody branch from f610a6b to da6cc53 Compare May 3, 2023 07:50
@Luthan95 Luthan95 force-pushed the customexception_retrofitErrorbody branch from 91cb58a to 9b8d7e0 Compare May 8, 2023 10:47
@Luthan95 Luthan95 force-pushed the customexception_retrofitErrorbody branch from 9b8d7e0 to 554f290 Compare May 8, 2023 13:36
@@ -69,8 +69,8 @@ public Response getResponse() {
* @throws RuntimeException wrapping the underlying IOException if unable to convert the body to
* the specified {@code type}.
*/
public <T> T getErrorBodyAs(Class<T> type) {
if (response == null || response.errorBody() == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need the null check, yes? Do we have a test that covers this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya, SpinnakerRetrofit2ErrorHandleTest covers that scenario

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you link to the specific test please? I don't see anything in SpinnakerRetrofit2ErrorHandleTest that calls getErrorBodyAs. It's hard to tell which test constructs a SpinnakerServerException with a RetrofitException built with a successful Response (i.e. one where response.errorBody is null here).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this is easier to test without all the rest endpoint stuff, like with a new RetrofitExceptionTest class.

Copy link
Contributor Author

@Luthan95 Luthan95 May 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test cases to RetrofitException and tried for Response.errorBody as null, but it is impossible to set response without error body, testcases helps to show those scenarios if we create response success response with otherthan success code it will throw IllegalArgumentException...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5f047fe shows that it is possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a better solution is to make the constructor private? And then assert that Response.errorBody() is not null in the constructor?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f0d5abe

@@ -69,10 +69,11 @@ public Response getResponse() {
* @throws RuntimeException wrapping the underlying IOException if unable to convert the body to
* the specified {@code type}.
*/
public <T> T getErrorBodyAs(Class<T> type) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a comment about an earlier line in this file, not modified (yet) in this PR. As I stare at this code some more, I noticed that Response is a generic type...So, instead of

private final Response response;

above, it's more correct to have

private final Response<T> response;

and then add T as a type parameter to RetrofitException and the other places Response is used.

We'd need a different letter here if we use T for that. Perhaps let's leave this for a followup PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe Response<?> is more appropriate?...

dbyron-sf and others added 2 commits May 19, 2023 14:11
so construction only happens via the RetrofitException's factory methods
@Luthan95 Luthan95 force-pushed the customexception_retrofitErrorbody branch from 7fc44db to 0018064 Compare May 22, 2023 06:24
SpinnakerServerException serverException = new SpinnakerServerException(retrofitException);

Map<String, Object> responseBody = serverException.getResponseBody();
assertEquals(responseBody, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assertEquals(responseBody, null);
assertNull(responseBody);

SpinnakerServerException serverException = new SpinnakerServerException(retrofitException);

Map<String, Object> responseBody = serverException.getResponseBody();
assertEquals(responseBody, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assertEquals(responseBody, null);
assertNull(responseBody);

"{\"name\"=\"test\"}")),
retrofit2Service);

assertThrows(RuntimeException.class, () -> retrofitException.getBodyAs(HashMap.class));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could use some comments here, or perhaps a more descriptive method name. Why does this test expect a RuntimeException?

.addConverterFactory(JacksonConverterFactory.create())
.build();

RetrofitException retrofitException =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see some more test coverage here, and potentially in RetrofitExceptionTest as well -- of http responses with non-200 errors codes that name non-json response bodies, as well as a json list.

import retrofit2.Converter;
import retrofit2.Retrofit;

public class ByteArrayConverterFactoryTestUtil extends Converter.Factory {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

javadoc please

@@ -78,6 +89,8 @@ public void testHttpConversionFail() {
"{\"name\"=\"test\"}")),
retrofit2Service);

// Gives MalformedJsonException or JsonEOFException or IllegalStateException, or
// some other exception for json parsing, which is a type of RuntimeException.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand why to expect these exceptions. Isn't the response body valid json?

ResponseBody errorBody = response.errorBody();
assertNull(errorBody);
assertThrows(
NullPointerException.class, () -> RetrofitException.httpError(response, retrofit2Service));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use some comments here explaining why to expect a NullPointerException.

String body = response.body();
assertNotNull(body);

// Assert the response body
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's clear from the code that this is what's happening. What's not clear is why.

}

@Test
public void test2xxSuccessResponseWithWildcard() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What additional coverage does this test give beyond test2xxSuccessResponseWithType?

this.testApiServiceForJsonResponse(expectedContentBodyMap, jsonContentString, null);
}

private void testApiServiceForJsonResponse(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

javadoc please

assertNotNull(jsonContentString);

String invalidContentJsonBodyString = (String) jsonContentString;
// validate contents cannot be null, otherwise it gives NullPointerException
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove duplicate comment?

Response<?> actualResponse = Response.error(HttpStatus.UNAUTHORIZED.value(), responseBody);
assertNotNull(actualResponse);

// assert response body is null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need to worry about actualResponse.body() since RetrofitException doesn't use it.

// Use default as GsonConverterFactory if not specified, for json list with lenient option to
// allow invalid json strings also
if (converterFactoryList == null || converterFactoryList.size() == 0) {
com.google.gson.Gson gson = new GsonBuilder().setLenient().create();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create this once and store in a variable outside this function?

Retrofit.Builder customRetrofitBuilder =
new Retrofit.Builder().baseUrl("http://localhost:8987/");

// Use default as GsonConverterFactory if not specified, for json list with lenient option to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Use default as GsonConverterFactory if not specified, for json list with lenient option to
// Use GsonConverterFactory if not specified, for json list with lenient option to

Please also explain the choice of GsonConverterFactory (it may be arbitrary). Is it the only one with a lenient option?

for (Object key : expectedContentBodyMap.keySet()) {
assertEquals(expectedContentBodyMap.get(key), actualResponseBodyContentMap.get(key));
}
} catch (RuntimeException exception) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what this catch-and-rethrow adds.

assertEquals(type, "UNKOWN_IOEXCEPTION_WHILE_SERVER_COMMUNICATION");

Map<String, String> responseBodyMap = retrofitException.getBodyAs(HashMap.class);
assertNull(responseBodyMap);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why expect null here?

MediaType.parse("application/json" + "; charset=utf-8"), "{\"name\":\"test\"}");
Response<?> actualResponse = Response.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), body);
assertNotNull(actualResponse);
networkException.initCause(new retrofit2.HttpException(actualResponse));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it important to do this?

}

@Test
public void testOrderOfConverterFactory() throws JsonProcessingException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we need to test retrofit2 internals. What would we do if this test starts failing?

String cause = exception.getCause().getMessage();
assertNotNull(cause);
assertNull(exception.getRetryable());
assertTrue(cause.contains("timeout"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this assertion telling us?

// create a simple invalid json string
// "{\"name\":\"te";// incomplete json gives com.fasterxml.jackson.core.io.JsonEOFException
String responseBodyString =
"{\"name\"=\"test\"}"; // com.fasterxml.jackson.core.JsonParseException
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't match the comments.

}

@Test
public void test200ResponseWithInvalidJson() throws Exception {
Copy link
Contributor

@dbyron-sf dbyron-sf May 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add a test of non-2xx (e.g. 500) response with invalid json.

@dbyron-sf dbyron-sf changed the title refactor(retrofit): add getErrorBodyAs() method to SpinnakerServerException feat(retrofit): add getErrorBodyAs() method to SpinnakerServerException Jun 5, 2023
@dbyron-sf dbyron-sf closed this Sep 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants