How can I handle empty response body with Retrofit 2?
AndroidRetrofitRetrofit2SquareAndroid Problem Overview
Recently I started using Retrofit 2 and I faced an issue with parsing empty response body. I have a server which responds only with http code without any content inside the response body.
How can I handle only meta information about server response (headers, status code etc)?
Android Solutions
Solution 1 - Android
Edit:
As Jake Wharton points out,
@GET("/path/to/get")
Call<Void> getMyData(/* your args here */);
is the best way to go versus my original response --
You can just return a ResponseBody
, which will bypass parsing the response.
@GET("/path/to/get")
Call<ResponseBody> getMyData(/* your args here */);
Then in your call,
Call<ResponseBody> dataCall = myApi.getMyData();
dataCall.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Response<ResponseBody> response) {
// use response.code, response.headers, etc.
}
@Override
public void onFailure(Throwable t) {
// handle failure
}
});
Solution 2 - Android
If you use RxJava, then it's better to use Completable
in this case
> Represents a deferred computation without any value but only indication for completion or exception. The class follows a similar event pattern as Reactive-Streams: onSubscribe (onError|onComplete)?
http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Completable.html
in the accepted answer:
@GET("/path/to/get")
Observable<Response<Void>> getMyData(/* your args here */);
If the endpoint returns failure response code, it will still be in the onNext
and you will have to check the response code yourself.
However, if you use Completable
.
@GET("/path/to/get")
Completable getMyData(/* your args here */);
you will have only onComplete
and onError
.
if the response code is success it will fire the onComplete
else it will fire onError
.
Solution 3 - Android
If you are using rxjava, use something like :
@GET("/path/to/get")
Observable<Response<Void>> getMyData(/* your args here */);
Solution 4 - Android
With kotlin, using the return type Call<Void>
still throws IllegalArgumentException: Unable to create converter for retrofit2.Call<java.lang.Void>
Using Response instead of Call resolved the issue
@DELETE("user/data")
suspend fun deleteUserData(): Response<Void>
Solution 5 - Android
Here is how I used it with Rx2 and Retrofit2, with PUT REST request: My request had a json body but just http response code with empty body.
The Api client:
public class ApiClient {
public static final String TAG = ApiClient.class.getSimpleName();
private DevicesEndpoint apiEndpointInterface;
public DevicesEndpoint getApiService() {
Gson gson = new GsonBuilder()
.setLenient()
.create();
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
okHttpClientBuilder.addInterceptor(logging);
OkHttpClient okHttpClient = okHttpClientBuilder.build();
apiEndpointInterface = new Retrofit.Builder()
.baseUrl(ApiContract.DEVICES_REST_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
.create(DevicesEndpoint.class);
return apiEndpointInterface;
}
The interface:
public interface DevicesEndpoint {
@Headers("Content-Type: application/json")
@PUT(ApiContract.DEVICES_ENDPOINT)
Observable<ResponseBody> sendDeviceDetails(@Body Device device);
}
Then to use it:
private void sendDeviceId(Device device){
ApiClient client = new ApiClient();
DevicesEndpoint apiService = client.getApiService();
Observable<ResponseBody> call = apiService.sendDeviceDetails(device);
Log.i(TAG, "sendDeviceId: about to send device ID");
call.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<ResponseBody>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(ResponseBody body) {
Log.i(TAG, "onNext");
}
@Override
public void onError(Throwable t) {
Log.e(TAG, "onError: ", t);
}
@Override
public void onComplete() {
Log.i(TAG, "onCompleted: sent device ID done");
}
});
}
Solution 6 - Android
You can try this one
Retrofit retrofit = new Retrofit.Builder().baseUrl(baseUrl)
.addConverterFactory(new NullOnEmptyConverterFactory())
.client(okHttpClient).build();
class NullOnEmptyConverterFactory extends Converter.Factory {
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
final Converter<ResponseBody, ?> delegate = retrofit.nextResponseBodyConverter(this, type, annotations);
return (Converter<ResponseBody, Object>) body -> {
if (body.source().exhausted()) return null;
return delegate.convert(body);
};
}
}
Solution 7 - Android
Here is an example Kotlin in MVVM with service, Repository and ViewModel:
Service:
@POST("/logout")
suspend fun logout(@Header("Authorization") token: String):Response<Unit>
Repository:
//logout
private val mLogoutResponse = MutableLiveData<String>()
val logoutResponse: LiveData<String>
get() {
return mLogoutResponse
}
suspend fun logout(token: String) {
try {
val result=quizzerProfileApi.logout(token)
if(result.code()!=0)
{
mLogoutResponse.postValue(result.code().toString())
}
} catch (e: Exception) {
Log.d("ProfileRepository", "logout: Error: $e")
}
}
ViewModel:
fun logout(token: String) {
viewModelScope.launch {
repository.logout(token)
}
}
val logoutResponseCd: LiveData<String>
get() = repository.logoutResponse
in Activity:
private fun logout() {
myViewModel.logout(token)
myViewModel.logoutResponseCd.observe(this, Observer {
if(it!="0"){
Log.d(TAG, "logout: code= $it")
finish()
}
else
Toast.makeText(this, "Error logging out: $it", Toast.LENGTH_SHORT).show()
})
}