I would like Retrofit to raise custom exceptions depending on the server response. For example in the following structure:
{
"code":0,
"message":"OK",
"data":{....}
}
I would like to raise an exception for subscribers if code is anything other than 0. How is it possible using Retrofit and Rx? I would much prefer to write this logic only once and have it applied to all observables returned by retrofit.
I would like to raise an exception for subscribers if code is anything other than 0. How is it possible using Retrofit and Rx?
You can use a Observable.flatMap operator:
api.request().flatMap(response -> {
if (response.getCode() != 0) {
return Observable.error(new Exception("Remote error occurred"));
}
return Observable.just(response);
});
I would much prefer to write this logic only once and have it applied to all observables returned by retrofit.
Unfortunately, there is not way to do it using retrofit and rx-java. You have to write the code above for every retrofit call. The only thing you can do is to use Observable.compose method and reduce the amount of boilerplate you actually have to write.
api.request().compose(new ResponseTransformer<Response>());
And here is the ResponseTransformer class:
public static class ResponseTransformer<T extends Response> implements Observable.Transformer<T, T> {
#Override
public Observable<T> call(final Observable<T> observable) {
return observable.flatMap(response -> {
if (response.getCode() != 0) {
return Observable.error(new Exception("Remote error occurred"));
}
return Observable.just(response);
});
}
}
UPDATE
Well, as I said, there is no way to avoid boilerplate code using only retrofit and rxjava, but you can workaround it with dynamic proxies (note that you don't need to call compose anymore):
final Api api = restAdapter.create(Api.class);
final ClassLoader loader = api.getClass().getClassLoader();
final Class<?>[] interfaces = api.getClass().getInterfaces();
final Api proxy = (Api) Proxy.newProxyInstance(loader, interfaces, new ResponseInvocationHandler(api));
proxy.request().subscribe(response -> {
System.out.println("Success!");
});
ResponseInvocationHandler class:
public static class ResponseInvocationHandler implements InvocationHandler {
private final Object target;
public ResponseInvocationHandler(final Object target) {
this.target = target;
}
#Override
#SuppressWarnings({"unchecked"})
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
final Object result = method.invoke(target, args);
if (result instanceof Observable) {
return Observable.class.cast(result).compose(new ResponseTransformer<>());
}
return result;
}
}
I would suggest a different approach.
You will need to implement a custom OkHttp client with custom Interceptor.
OkHttpClient client = new OkHttpClient();
client.interceptors().add(new MyInterceptor());
mAdapter = new RestAdapter.Builder().setEndpoint(Consts.ENDPOINT).setClient(new OkClient(client))
.setLogLevel(RestAdapter.LogLevel.BASIC).build();
In your interceptor depending on the code returned you can proceed normally or throw an exception.
Something like this:
public class MyInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
if(response.code() == 0) {
throw new RuntimeException("Something went wrong!");
}
return response;
}
}
1 custom Observable.Operator :
public class YourOperator implements Observable.Operator{
public void onNext(Data data){
if (data.code != 0 ){
//raise your custom Exception
}
}
public void onError(Throwable e){
//handler Exception
}
}
2 user like:
api.youRequest()
.lift(new YourOperator())
.subscribe(....);
Related
I am using Retrofit 2 and the json that i am getting back if its a successful call looks something like this
{
a:123,
b:456,
c:789
}
But when there is an error being returned from the server the error json looks like
{
error:"Error Message"
}
The error here is not an error in the connection,the call completed successfully.How do i configure retrofit to process this so i know if i received the first json or the second and display a message accordingly.
You could use a BaseResponse class and the rest of your responses class inherits from this class.
For example:
public class BaseResponse implements Serializable {
#SerializedName("error")
public String error;
}
You probably need model something like:
public class Example {
public int a;
public int b;
public int c;
#Nullable public String error;
public boolean isError() {
return error != null && !error.isEmpty();
}
}
and then in retrofit callback just check
Example example = response.body();
if (example.isError) {
// show error message
} else {
// show successfull response
}
Before using rx.Observable, I used a custom callback with retrofit so I can add some specific logic for handling response/error and not have to do that inside the callback for every request as boilerplate code.
I force users to use the custom callback by putting it in the method signature like this:
#GET("/user_endpoint/")
void getUser(CustomCallback<User> callback);
#GET("/profile_endpoint/")
void getProfile(CustomCallback<Profile> callback);
but now that I'm returning an Observable:
#GET("/user_endpoint/")
Observable<User> getUser();
#GET("/profile_endpoint/")
Observable<Profile> getProfile();
I can't figure out a way to make sure that a custom callback always proxies the error/response.
Also, with retrofit2.0, how can I force the user to use a custom callback with the returned Call object?
CustomCallback for reference:
public abstract class CustomCallback<T> implements Callback<T> {
#Override public final void success(T t, Response response) {
// do some logic
onSuccess(t);
}
#Override public final void failure(RetrofitError error) {
// do something with the error here such as show a Toast
Toast.makeText(Application.getInstance(), error.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
onFailure(error);
}
public abstract void onSuccess(T response);
public abstract void onFailure(Throwable error);
}
Stop. You're thinking this the wrong way.
Instead consider this: You have the normal Retrofit interface:
interface Foo {
#GET("/user_endpoint/")
Observable<User> getUser();
}
And then you have your decorator class:
public class FooDecorator implements Foo {
private Foo delegate = ...; // inject or create the Retrofit instance.
#Override
public Observable<User> getUser() {
return delegate.getUser().doOnNext(...).doOnError(...);
}
}
Then you use only the second class everywhere in your code (preferably just let the DI system use that) and you're set.
If you're feeling adventurous, you could even adapt the RxJavaCallAdapterFactory so that it modifies the returned observables without the need of a custom class.
We are trying to migrate to using Retrofit2 and I am having trouble with a requirement where we need to pass a set of dynamically generated headers (used for analytics) for every request.
#Headers is not supported at parameter level and since header field name vary based on the current activity, I cannot use #Header.
Is there a way to append the headers just before execute() ? (Looking for something similar to #QueryMap/#FieldMap but for headers)
NOTE: I do not have the list of headers while initializing the client (and hence cannot use the Interceptor to do this).
You still can (and have to) use the Interceptor.
All you need is a little Architecture.
First create a helper that provides the necessary headers.
public class AnalyticsHeader {
private String analyticsHeaderName;
private String analyticsHeaderValue;
public void setHeaderValue(String header) {
this.analyticsHeaderValue = header;
}
public void setHeaderName(String header) {
this.analyticsHeaderName = header;
}
public String getHeaderName() {
return analyticsHeaderName;
}
public String getHeaderValue() {
return analyticsHeaderValue;
}
}
Keep an instance of this class in a accessible place inside your app, for example the MainActivity our the Application (or even better, use Dependency Injection)
Now, upon creation of the Interceptor just pass the instance of the AnalyticsHeader into the Interceptor:
public static final class AnalyticsInterceptor implements Interceptor {
private final AnalyticsHeader header;
public AnalyticsInterceptor(AnalyticsHeader header) {
this.header = header;
}
#Override
public Response intercept(Chain chain) throws IOException {
final Request original = chain.request();
Response response;
if (header.getHeader() != null) {
Request request = original.newBuilder()
.header(header.getHeaderName(), header.getHeaderValue())
.method(original.method(), original.body())
.build();
response = chain.proceed(request);
} else {
response = chain.proceed(original);
}
return response;
}
}
And then...
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addInterceptor(new AnalyticsInterceptor(CentralPlaceInApp.getAnalyticsHeader());
...
retrofit = new Retrofit.Builder()
.baseUrl(config.getRestUrl())
.client(builder.build())
.build();
Now, you can change the value of the header anytime during your app runtime using CentralPlaceInApp.getAnalyticsHeader().setHeaderValue(CurrentActivity.class.getSimpleName());
Imagine the following request:
#POST("/recipes/create")
void createRecipe(#Query("recipe") Recipe recipe, Callback<String> callback);
I would like to have toJson(recipe) but unfortunately my request is just calling toString() for my recipe which does not work at all.
I could override the toString inside of Recipe but I'd rather have a general solution.
I cannot use #Body as I need to specify, what I'm sending (i need to have "recipe=json(theRecipe)".
I also cannot change the serialization to add "recipe=" as I'm not in charge of the server.
At the moment I'm using a QueryMap Map where I put in a serialized object. Although this works, it's not a very nice solution in my opinion.
Can I somehow intercept the retrofit-adapter?
This is now possible when registering a custom Converter.Factory that overrides the stringConverter method, which is called when resolving parameters. The Github issue that #William referred to 2 years ago doesn't seem to be updated since support was added.
Method Javadoc:
Returns a Converter for converting type to a String, or null if type cannot be handled by this factory. This is used to create converters for types specified by #Field, #FieldMap values, #Header, #HeaderMap, #Path, #Query, and #QueryMap values.
The example below delegates to Gson, but in the same way any type of conversion can be applied to the parameters.
Example: GsonStringConverterFactory
class GsonStringConverterFactory
extends Converter.Factory {
private final transient Gson gson;
GsonStringConverterFactory(final Gson gson) {
this.gson = gson;
}
#Override
public Converter<?, String> stringConverter(final Type type, final Annotation[] annotations, final Retrofit retrofit) {
final TypeAdapter typeAdapter;
typeAdapter = gson.getAdapter(TypeToken.get(type));
return new StringConverter<>(typeAdapter);
}
private static class StringConverter<T>
implements Converter<T, String> {
private final TypeAdapter<T> typeAdapter;
private StringConverter(final TypeAdapter<T> typeAdapter) {
this.typeAdapter = typeAdapter;
}
#Override
public String convert(final T value)
throws IOException {
/* This works in our case because parameters in this REST api are always some kind of scalar
* and the toJson method converts these to simple json types. */
final String jsonValue;
jsonValue = typeAdapter.toJson(value));
if (jsonValue.startsWith("\"") && jsonValue.endsWith("\"") {
/* Strip enclosing quotes for json String types */
return jsonValue.substring(1, jsonValue.length() - 1);
} else {
return jsonValue;
}
}
}
}
Registering the converter:
To register the custom converter, your Retrofit builder could look something like this:
new Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.addConverterFactory(new GsonStringConverterFactory(gson))
.build();
I don't think it supports this right now in some nice way. Check this answer by one of the authors: https://github.com/square/retrofit/issues/291
The suggested method from that answer is to create a custom type that overrides the toString() method, because Retrofit internally uses String.valueOf(value) to convert query or path parameters to strings.
So, you could have something like this:
class Recipe {
public int id;
public String title;
#Override
public String toString() {
// You can use a GSON serialization here if you want
// This is not a robust implementation
return Integer.toString(id) + "-" + title;
}
}
As #Rolf mentioned, there is a way to set customConverter.Factory
Example:
public class QueryConverterFactory extends Converter.Factory {
public static QueryConverterFactory create() {
return new QueryConverterFactory();
}
private QueryConverterFactory() {
}
#Nullable
#Override
public Converter<?, String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
if (type == Date.class) {
return DateQueryConverter.INSTANCE;
}
return null;
}
private static final class DateQueryConverter implements Converter<Date, String> {
static final DateQueryConverter INSTANCE = new DateQueryConverter();
private static final ThreadLocal<DateFormat> DF = new ThreadLocal<DateFormat>() {
#Override
public DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
#Override
public String convert(Date date) {
return DF.get().format(date);
}
}
}
You can add converters for your own types.
You can use retrofit.RestAdapter.Builder().setConverter(...) to pass a custom json converter.
I am having trouble with chaining observables using retrofit's RxJava support. I'm probably misunderstanding how to use it, otherwise it could be a bug in retrofit. Hopefully someone here can help me understand what's going on. Edit: I am using the MockRestAdapter for these responses - this might be relevant as I see the RxSupport implementations differ slightly.
This is a fake banking app. It's trying to do a transfer, and after the transfer is completed, then it should do a accounts request to update the account values. This is basically just an excuse for me to try out flatMap. The following code unfortunately doesn't work, no subscribers ever get notified:
Case 1: chaining two retrofit-produced observables
The transfer service (note: returns a retrofit-produced observable):
#FormUrlEncoded #POST("/user/transactions/")
public Observable<TransferResponse> transfer(#Field("session_id") String sessionId,
#Field("from_account_number") String fromAccountNumber,
#Field("to_account_number") String toAccountNumber,
#Field("amount") String amount);
The account service (note: returns a retrofit-produced observable):
#FormUrlEncoded #POST("/user/accounts")
public Observable<List<Account>> getAccounts(#Field("session_id") String sessionId);
Chains two retrofit-produced observables together:
transfersService.transfer(session.getSessionId(), fromAccountNumber, toAccountNumber, amount)
.flatMap(new Func1<TransferResponse, Observable<? extends List<Account>>>() {
#Override public Observable<? extends List<Account>> call(TransferResponse transferResponse) {
return accountsService.getAccounts(session.getSessionId());
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
Case 2: creating my own observable and chaining with a retrofit-produced one
If I ignore the built in Rx support in Retrofit for the "flat mapped" call, it works perfectly! All subscribers get notified. See below:
The new accounts service (note: does not produce an observable):
#FormUrlEncoded #POST("/user/accounts")
public List<Account> getAccountsBlocking(#Field("session_id") String sessionId);
Create my own observable and emit the items myself:
transfersService.transfer(session.getSessionId(), fromAccountNumber, toAccountNumber, amount)
.flatMap(new Func1<TransferResponse, Observable<? extends List<Account>>>() {
#Override public Observable<? extends List<Account>> call(TransferResponse transferResponse) {
return Observable.create(new Observable.OnSubscribe<List<Account>>() {
#Override public void call(Subscriber<? super List<Account>> subscriber) {
List<Account> accounts = accountsService.getAccountsBlocking(session.getSessionId());
subscriber.onNext(accounts);
subscriber.onCompleted();
}
});
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
Any help would be greatly appreciated!
The answer is yes you should be able to chain observables from Retrofit. There seems to be a bug in the MockRestAdapter$MockRxSupport:createMockObservable private class. The way the scheduling is done with respect to subscribing the subscriber to the observable seems wrong. Subscribing to the observable comes after in the HttpExecutor thread itself is started. I believe the original flow which comes from your Schedulers.io() thread is completed and unsubscribed before the mockHandler.invokeSync returned Observable can be subscribed to. Hopefully this explanation make some sort of sense if you take a look at the code in the retrofit-mock module.
As a workaround for now with the current code when using the retrofit-mock only you could substitute the internal default Executor with your own ImmediateExecutor implementation. This would allow at least when testing mocks to have a single thread flow which would be provided by your Schedulers.io.
// ImmediateExecutor.java
public class ImmediateExecutor implements Executor {
#Override
public void execute(Runnable command) {
command.run();
}
}
// Create your RestAdapter with your ImmdiateExecutor
RestAdapter adapter = new RestAdapter.Builder()
.setEndpoint(endpoint)
.setExecutors(new ImmediateExecutor(), null)
.build();
As for fixing the issue at the source you can also include the retrofit-mock project as source in your project and modify the MockRestAdapter$MockRxSupport:createMockObservable method using the code below. I've tested your use-case and it does fix the problem.
--- MockRestAdapter.java$MockRxSupport ----
Observable createMockObservable(final MockHandler mockHandler, final RestMethodInfo methodInfo,
final RequestInterceptor interceptor, final Object[] args) {
return Observable.create(new Observable.OnSubscribe<Object>() {
#Override public void call(final Subscriber<? super Object> subscriber) {
try {
if (subscriber.isUnsubscribed()) return;
Observable observable =
(Observable) mockHandler.invokeSync(methodInfo, interceptor, args);
observable.subscribeOn(Schedulers.from(httpExecutor));
//noinspection unchecked
observable.subscribe(subscriber);
} catch (RetrofitError e) {
subscriber.onError(errorHandler.handleError(e));
} catch (Throwable e) {
subscriber.onError(e);
}
}
});
}
Created an issue with the Retrofit project here, we'll see if they accept it.