-
Notifications
You must be signed in to change notification settings - Fork 173
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
added retrofit calladaptor to handle exception globally
- Loading branch information
Showing
2 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
223 changes: 223 additions & 0 deletions
223
...ain/java/com/netflix/spinnaker/kork/retrofit/ErrorHandlingExecutorCallAdapterFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
109 changes: 109 additions & 0 deletions
109
...rofit/src/main/java/com/netflix/spinnaker/kork/retrofit/exceptions/RetrofitException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |