I'm using retrofit 2 along with rx java
Situation:
the app sends some request, then i get the response in json-format that is automatically converted to User dto, then in onNext method of rx java i receive the list of Users. What if i get some message from server like this: {"error":"can't get the list of users"}
how to handle this situation with retrofit 2 and rx?
Subscription subscriptionBranches = model.getRepoBranches(owner, name)
.map(branchesMapper)
.subscribe(new Observer<List<Branch>>() {
#Override
public void onCompleted() {
;
}
#Override
public void onError(Throwable e) {
if (e instanceof retrofit.HttpException) {
HttpException exception = (HttpException) e;
}
showError(e);
}
#Override
public void onNext(List<Branch> list) {
branchList = list;
view.showBranches(list);
}
});
addSubscription(subscriptionBranches);
.....
#Override
public Observable<List<RepositoryDTO>> getRepoList(String name) {
return apiInterface
.getRepositories(name)
.compose(applySchedulers());
}
Depending on the server response you might or might not get into your onError function. If the server returns a non-2XX http status code you'll get into the onError method. If on the other hand you get a 2XX http status code you'll enter onNext.
I'm assuming you can deal with the onNext bit and I'll explain how you can do it in the onError. It's important to realise that there are many ways of doing this and this is just an example that uses okhttp 3 and retrofit 2 beta4.
So retrofit2 says that every non-2XX http responses are HttpExceptions when using rxjava. This you already have it there in your code:
if (e instanceof retrofit.HttpException) {
HttpException exception = (HttpException) e;
}
Now what you want to do is get the body of the response. This you can achieve by calling Response response = exception.response() in the HttpException you have there. With the response, getting the error body is quite straight forward. You just call response.errorBody(). You can then convert the body to a java object or just access it as a string.
Since you have a json error body as an example, here's how you can convert the response body to a java object:
new GsonConverterFactory().responseBodyConverter(type,
new Annotation[0]).convert(response.errorBody());
where type is the class of the java object that represents the error.
So putting it all together, on your onError method you could write something like:
if (e instanceof retrofit.HttpException) {
HttpException exception = (HttpException) e;
Response response = exception.response();
Converter<ResponseBody, MyError> converter = new GsonConverterFactory()
.responseBodyConverter(MyError.class, Annotation[0]);
MyError error = converter.convert(response.errorBody());
}
MyError is a model that represents the error json you have in your question.
I believe in the case you mentioned you will just enter into your onError handling, because retrofit will fail to deserialize your response, as it's not formatted as a List. You could potentially handle your case through that based off of the exception type.
If you can't alter the api to return consistent response types, you will have to look into using TypedInput, and possibly a converter.
Additionally, while it may not be completely relevant/overkill to the situation at hand, TypeAdapters bear mentioning. They'll let you determine how retrofit deserializes gson on a per class basis.
Gson gson = new GsonBuilder()
.registerTypeAdapter(MyClass.class, new MyAdapter())
.create();
RestAdapter adapter = new RestAdapter.Builder()
.setConverter(new GsonConverter(gson))
.build();
I had the same situation and the way I could get the json from the server when an error occurs was something like this:
retrofit2.Response<TokenRefresh> r = call.execute();
String errorMessage = "";
try {
errorMessage = r.errorBody().string();
} catch (IOException e) {
e.printStackTrace();
}
Timber.d("errorMessage: " + errorMessage);
Related
I'm new to using Retrofit for networking in Android. So far I have successfully written code that sends a String to a PHP server. The server is configured to take my string, which contains a system ID and a password ("systemid=id&password=password"). It may seem odd to append parameters via a String like this but this is the way I have to do it for this project. Anyway, on the server if the id and password sent to it match certain criteria, it's sends back a list of file locations. So far, I am getting the proper response back from the server. In other words 'response.getMessage()' returns 'OK.' But how can I get the list that the server is trying to send back? Here is my implementation so far.
Interface:
public interface ExampleClient {
#POST("login/fake_name.php")
Call<String> sendStringToServer(#Body String string);
}
Here is my code within the Activity:
private void sendRequestToServer(String str) {
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl("http://www.example.com/")
.addConverterFactory(GsonConverterFactory.create());
Retrofit retrofit = builder.build();
PromoterKiosksClient client = retrofit.create(ExampleClient.class);
Call<List<String>> call = client.sendStringToServer(str);
call.enqueue(new Callback<Void>() {
#Override
public void onResponse(Call<String> call, Response<String> response) {
Log.d(TAG, response.toString());
Log.d(TAG, response.message());
}
#Override
public void onFailure(Call<String> call, Throwable t) {
Toast.makeText(AdminActivity.this, "you suck! keep trying", Toast.LENGTH_SHORT).show();
}
});
}
Is there somewhere in onResponse() that I can do this or do I need a totally different implementation? Thanks in advance
You are looking for response.body() ? Do you get a JSON response from the server? If so you can use POJO to convert your response. Would you be able to share your response here? You can use POJO to convert your result and name it Result.java and set that as your return type.
private void sendRequestToServer(String str) {
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl("http://www.example.com/")
.addConverterFactory(GsonConverterFactory.create());
Retrofit retrofit = builder.build();
PromoterKiosksClient client = retrofit.create(ExampleClient.class);
Call<Result> call = client.sendStringToServer(str);
call.enqueue(new Callback<Void>() {
#Override
public void onResponse(Call<Result> call, Response<Result> response) {
Log.d(TAG, response.toString());
Log.d(TAG, response.message());
response.body(); // have your all data
}
#Override
public void onFailure(Call<Result> call, Throwable t) {
Toast.makeText(AdminActivity.this, "you suck! keep trying", Toast.LENGTH_SHORT).show();
}
});
}
Call<User> vs. Call<ResponseBody> vs. Call<Void>
Most endpoints will be declared with a specific return type, like Call. In this case Retrofit will always take the response body and try to convert it to Java objects. Of course, this takes time, memory and processing power.
If you can live without the mapped Java objects, you should choose Call. This makes the raw response payload available to you, but skips the mapping to Java objects. With this option, you still have the chance to analyze the payload (e.g., JSON).
The most efficient way is Call because it not only skips the conversion to Java objects, it also ignores the response body payload. When the response body is very large (e.g., a large JSON or an image), you can save a little bit of extra time and battery consumption by using Call. Of course, this results in the body() method of your response object returning null.
A good example to look at Consuming an API with retrofit and RXJava
I am using Retrofit library (version 2.0.2 as of this writing).
I am making a GET call to a service which responds a big JSON object but I am only interested in one key:value pair in it.
How can I get just that instead of writing a whole new POJO class that matches the JSON response?
Example -
{
status_code: 34,
status_message: "The resource you requested could not be found.",
...,
...
}
I need only status code value (34 here).
Please note, I am just giving an example of this JSON object here. The real one I am dealing with is huge and I care about only one key:value pair in it.
Thanks in advance.
You can refer to the following:
#GET("/files/jsonsample.json")
Call<JsonObject> readJsonFromFileUri();
and
class MyStatus{
int status_code;
}
...
Retrofit retrofit2 = new Retrofit.Builder()
.baseUrl("http://...")
.addConverterFactory(GsonConverterFactory.create())
.build();
WebAPIService apiService = retrofit2.create(WebAPIService.class);
Call<JsonObject> jsonCall = apiService.readJsonFromFileUri();
jsonCall.enqueue(new Callback<JsonObject>() {
#Override
public void onResponse(Call<JsonObject> call, Response<JsonObject> response) {
String jsonString = response.body().toString();
Gson gson = new Gson();
MyStatus status = gson.fromJson(jsonString, MyStatus.class);
Log.i(LOG_TAG, String.valueOf(status.status_code));
}
#Override
public void onFailure(Call<JsonObject> call, Throwable t) {
Log.e(LOG_TAG, t.toString());
}
});
...
Debug screenshot
When REST Api returns 404, onResponse called with NULL Response. We have user search api and if the user is not found on the server our REST api returns 404 with error response in body with more details.
As Retrofit 2.0 returns a null response body, it's hard for us to show the correct error to the user.
Do we have an alternate solution to get a response during 404?
Response:
{"responseStatus":{"code":"00297","severity":"ERROR","message":"Profile not found..","info":"","status":404}}
HTTP Status Code:
404
Thanks
Retrofit 2.0 shouldn't be returning a null response. If you are making an async callback and want the handle the error it should look something like this
// Create a very simple REST adapter which points the GitHub API.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
// Create an instance of our GitHub API interface.
GitHub github = retrofit.create(GitHub.class);
// Create a call instance for looking up Retrofit contributors.
Call<List<Contributor>> call = github.contributors("square", "retrofit");
call.enqueue(new Callback<ArrayList<Item>>() {
#Override
public void onResponse(Call<ArrayList<Item>> call, Response<ArrayList<Item>> response) {
if(response.isSuccessful()) {
// do something
}
else {
Log.e("Error Code", String.valueOf(response.code()));
Log.e("Error Body", response.errorBody().toString());
//display the appropriate message...
}
}
#Override
public void onFailure(Call<ArrayList<Item>> call, Throwable t) {
}
});
Some of that code was copied from Retrofit Samples. If you are getting a null response on a 404 something else is going wrong.
My web server returns JSON in the following form if successful (200 status code):
{"success":true,"data":{"user_id":"20","username":"Bob"}}
But if something went wrong (for example, if the username and password entered was too short in a form), it returns JSON in the following form (400 status code):
{"success":true,"errors":{"username":["Username too short"],"password":["Incorrect password","Password too short"]}}
How do I handle these two different responses in Retrofit? This is my code so far:
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(URL)
.build();
ApiEndpointInterface api = restAdapter.create(ApiEndpointInterface.class);
api.getToken('1', new Callback<DefaultResponse>() {
#Override
public void success(DefaultResponse json, Response response) {
//
}
#Override
public void failure(RetrofitError response) {
//
}
});
How do I handle these two differently structured JSON responses?
You need to do two things.
First, do not return a 400 from the server. as others have stated you actually have a successful response from the server, so you just need to parse the error on your end.
your server can return a flag indicating login success or failure.
After that, you can simply add the error model to your DefaultResponse model. like so
public class DefaultResponse {
public boolean success;
public DataModel data;
public ErrorModel errors;
public static class DataModel {
private String user_id;
private String username;
}
public static class ErrorModel { ... }
}
You will still need to handle instances where your server responds with statuses other than OK, but typically error responses from a server are simple plain text, like "bad request".. You could of course insert json if you wish which you could extract from RetrofitError.getMessage(), but GSON wouldn't parse that for you automatically. I hope this helps!
You should realize that actually both responses you posted are success from the network request perspective.
The failure status is for the request failure (Network connection error, 404, etc.) and not failure you return after trying to login the user in your backend.
You'll need to parse the result in success method and decide weather the internal result is success from your app logic perspective.
You should offcourse also handle failure().
if you get the JSON from your web service, which mean it's success.
so, in your success method, grab the JSON and handle it
for example, from your JSON format
#Override
public void success(DefaultResponse json, Response response) {
try {
JSONObject json = new JSONObject(json);
JSONObject data = json.get("data"); // if no "data" element, it will throw JSONException
} catch (JSONException e) {
e.printStackTrace();
Toast.makeText(getApplicationContext(),"username to short",Toast.LENGTH_SHORT).show();
}
}
or you can use GSON to handle your JSON data
I am using Retrofit (1.6.1) with Robospice (1.4.14) to get data from some services (response should be in JSON).
In some cases, I might receive a HTML error page instead of a JSON response. The server returns a 200 status code, and I can not change that. In such cases, RoboSpice will call the onRequestFailure(SpiceException) method.
There, I am able to get the original RetrofitError excpetion, but the body is null. This is how I get it:
if (spiceException.getCause() instanceof RetrofitError) {
RetrofitError error = (RetrofitError) spiceException.getCause();
error.getBody(); // returns null
}
After investigating the source code of Retrofit, I found out that the body is replaced with null if the conversion fails (which is the case here, as Retrofit expect JSON and receives HTML).
The following line in RestAdapter is the source of my issue:
response = Utils.replaceResponseBody(response, null);
Is there a way to not set the body to null? In an other SO question, I found that if the server returns 4xx, the body is kept, but I can not change that.
You should probably create a retrofitted method that will just return a retrofit.client.Response and manually call conversion on it if the response body is in the necessary format.
Your Retrofit interface:
...
#GET("/foo/bar")
Response fooBarMethod(Object foo, Object bar);
...
Your RoboSpice request:
...
#Override
public final FooBar loadDataFromNetwork() throws Exception {
Response r = getService().fooBarMethod(foo, bar);
if (isBodyInHtmlFormat()) {
// cool stuff
throw new ResponseIsHtmlException();
} else {
// it is wise to make sure that it is
// exactly the same converter you are passing to
// your RetrofitSpiceService
Converter converter = createGsonConverter();
return (FooBar) converter.fromBody(response.getBody(), FooBar.class);
}
}