Skip to content

Commit

Permalink
added retrofit calladaptor to handle exception globally
Browse files Browse the repository at this point in the history
  • Loading branch information
Luthan95 committed Jan 9, 2023
1 parent a37d5c6 commit eb2662e
Show file tree
Hide file tree
Showing 2 changed files with 332 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* Copyright 2023 Netflix, Inc.
*
* 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 com.netflix.spinnaker.kork.retrofit;

import com.netflix.spinnaker.kork.retrofit.exceptions.RetrofitException;
import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerHttpException;
import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerNetworkException;
import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerServerException;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.concurrent.Executor;
import okhttp3.Request;
import okio.Timeout;
import org.jetbrains.annotations.Nullable;
import org.springframework.http.HttpStatus;
import retrofit2.*;

public class ErrorHandlingExecutorCallAdapterFactory extends CallAdapter.Factory {

private final Executor callbackExecutor;

ErrorHandlingExecutorCallAdapterFactory(Executor callbackExecutor) {
this.callbackExecutor = callbackExecutor;
}

@Nullable
@Override
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {

if (getRawType(returnType) != Call.class) {
return null;
}
final Type responseType = getCallResponseType(returnType);
return new CallAdapter<Object, Call<?>>() {
@Override
public Type responseType() {
return responseType;
}

@Override
public Call<Object> adapt(Call<Object> call) {
return new ExecutorCallbackCall<>(callbackExecutor, call, retrofit);
}
};
}

static final class ExecutorCallbackCall<T> implements Call<T> {

final Executor callbackExecutor;
final Call<T> delegate;
private final Retrofit retrofit;

ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate, Retrofit retrofit) {
this.callbackExecutor = callbackExecutor;
this.delegate = delegate;
this.retrofit = retrofit;
}

@Override
public Response<T> execute() {
try {
Response<T> syncResp = delegate.execute();
if (syncResp.isSuccessful()) {
return syncResp;
}
SpinnakerHttpException retval =
new SpinnakerHttpException(
RetrofitException.httpError(
syncResp.raw().request().url().toString(), syncResp, retrofit));
if ((syncResp.code() == HttpStatus.NOT_FOUND.value())
|| (syncResp.code() == HttpStatus.BAD_REQUEST.value())) {
retval.setRetryable(false);
}
throw retval;
} catch (Exception e) {
if (e instanceof IOException) {
throw new SpinnakerNetworkException(RetrofitException.networkError((IOException) e));
} else {
throw new SpinnakerServerException(RetrofitException.unexpectedError(e));
}
}
}

@Override
public void enqueue(Callback<T> callback) {
checkNotNull(callback, "callback == null");
delegate.enqueue(
new MyExecutorCallback<>(callbackExecutor, delegate, callback, this, retrofit));
}

@Override
public boolean isExecuted() {
return delegate.isExecuted();
}

@Override
public void cancel() {
delegate.cancel();
}

@Override
public boolean isCanceled() {
return delegate.isCanceled();
}

@Override
public Call<T> clone() {
return new ExecutorCallbackCall<>(callbackExecutor, delegate.clone(), retrofit);
}

@Override
public Request request() {
return delegate.request();
}

@Override
public Timeout timeout() {
return delegate.timeout();
}
}

static class MyExecutorCallback<T> implements Callback<T> {
final Executor callbackExecutor;
final Call<T> delegate;
final Callback<T> callback;
final ExecutorCallbackCall<T> executorCallbackCall;
private Retrofit retrofit;

public MyExecutorCallback(
Executor callbackExecutor,
Call<T> delegate,
Callback<T> callback,
ExecutorCallbackCall<T> executorCallbackCall,
Retrofit retrofit) {
this.callbackExecutor = callbackExecutor;
this.delegate = delegate;
this.callback = callback;
this.executorCallbackCall = executorCallbackCall;
this.retrofit = retrofit;
}

@Override
public void onResponse(final Call<T> call, final Response<T> response) {
if (response.isSuccessful()) {
callbackExecutor.execute(
new Runnable() {
@Override
public void run() {
callback.onResponse(executorCallbackCall, response);
}
});
} else {
callbackExecutor.execute(
new Runnable() {
@Override
public void run() {

SpinnakerHttpException retval =
new SpinnakerHttpException(
RetrofitException.httpError(
response.raw().request().url().toString(), response, retrofit));
if ((response.code() == HttpStatus.NOT_FOUND.value())
|| (response.code() == HttpStatus.BAD_REQUEST.value())) {
retval.setRetryable(false);
}

callback.onFailure(executorCallbackCall, retval);
}
});
}
}

@Override
public void onFailure(Call<T> call, final Throwable t) {

SpinnakerServerException exception;
if (t instanceof IOException) {
exception = new SpinnakerNetworkException(RetrofitException.networkError((IOException) t));
} else {
exception = new SpinnakerServerException(RetrofitException.unexpectedError(t));
}
final SpinnakerServerException finalException = exception;
callbackExecutor.execute(
new Runnable() {
@Override
public void run() {
callback.onFailure(executorCallbackCall, finalException);
}
});
}
}

static Type getCallResponseType(Type returnType) {
if (!(returnType instanceof ParameterizedType)) {
throw new IllegalArgumentException(
"Call return type must be parameterized as Call<Foo> or Call<? extends Foo>");
}
return getParameterUpperBound(0, (ParameterizedType) returnType);
}

static <T> T checkNotNull(@Nullable T object, String message) {
if (object == null) {
throw new NullPointerException(message);
}
return object;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2023 Netflix, Inc.
*
* 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 com.netflix.spinnaker.kork.retrofit.exceptions;

import java.io.IOException;
import java.lang.annotation.Annotation;
import okhttp3.ResponseBody;
import retrofit2.Converter;
import retrofit2.Response;
import retrofit2.Retrofit;

public class RetrofitException extends RuntimeException {
public static RetrofitException httpError(String url, Response response, Retrofit retrofit) {
String message = response.code() + " " + response.message();
return new RetrofitException(message, url, response, Kind.HTTP, null, retrofit);
}

public static RetrofitException networkError(IOException exception) {
return new RetrofitException(exception.getMessage(), null, null, Kind.NETWORK, exception, null);
}

public static RetrofitException unexpectedError(Throwable exception) {
return new RetrofitException(
exception.getMessage(), null, null, Kind.UNEXPECTED, exception, null);
}

/** Identifies the event kind which triggered a {@link RetrofitException}. */
public enum Kind {
/** An {@link IOException} occurred while communicating to the server. */
NETWORK,
/** A non-200 HTTP status code was received from the server. */
HTTP,
/**
* An internal error occurred while attempting to execute a request. It is best practice to
* re-throw this exception so your application crashes.
*/
UNEXPECTED
}

private final String url;
private final Response response;
private final Kind kind;
private final Retrofit retrofit;

RetrofitException(
String message,
String url,
Response response,
Kind kind,
Throwable exception,
Retrofit retrofit) {
super(message, exception);
this.url = url;
this.response = response;
this.kind = kind;
this.retrofit = retrofit;
}

/** The request URL which produced the error. */
public String getUrl() {
return url;
}

/** Response object containing status code, headers, body, etc. */
public Response getResponse() {
return response;
}

/** The event kind which triggered this error. */
public Kind getKind() {
return kind;
}

/** The Retrofit this request was executed on */
public Retrofit getRetrofit() {
return retrofit;
}

/**
* HTTP response body converted to specified {@code type}. {@code null} if there is no response.
*
* @throws 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) {
return null;
}
Converter<ResponseBody, T> converter = retrofit.responseBodyConverter(type, new Annotation[0]);
try {
return converter.convert(response.errorBody());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

0 comments on commit eb2662e

Please sign in to comment.