Reputation: 426
I am using Retrofit 2 in my Android project. When I hit an API endpoint using a GET method and it returns a 400 level error I can see the error content when I use an HttpLoggingInterceptor, but when I get to the Retrofit OnResponse callback the error body's string is empty.
I can see that there is a body to the error, but I can't seem to pull that body when in the context of the Retrofit callback. Is there a way to ensure the body is accessible there?
Thanks, Adam
Edit: The response I see from the server is: {"error":{"errorMessage":"For input string: \"000001280_713870281\"","httpStatus":400}}
I am trying to pull that response from the response via:
BaseResponse baseResponse = GsonHelper.getObject(BaseResponse.class, response.errorBody().string());
if (baseResponse != null && !TextUtils.isEmpty(baseResponse.getErrorMessage()))
error = baseResponse.getErrorMessage();
(GsonHelper is just a helper which passes the JSON string through GSON to pull the object of type BaseResponse
)
The call to response.errorBody().string() results in an IOException: Content-Length and stream length disagree, but I see the content literally 2 lines above in Log Cat
Upvotes: 12
Views: 5964
Reputation: 782
Use the code below to get error body string from response
. It can be used repeatedly and will not return an empty string after the first use.
Kotlin:
fun Response<*>.getErrorBodyString(): String? {
val peekSource = errorBody()?.source()?.peek()
return peekSource?.let {
val defaultCharset = errorBody()?.contentType()?.charset(UTF_8) ?: UTF_8
val charset = it.readBomAsCharset(defaultCharset)
// Use the line below in older versions of okhttp
// val charset = Util.bomAwareCharset(it, defaultCharset)
it.readString(charset)
}
}
Java:
public static String getErrorBodyString(Response<?> response) throws IOException {
// Get a copy of error body's BufferedSource.
BufferedSource peekSource = response.errorBody().source().peek();
// Get the Charset, as in the original response().errorBody().string() implementation
MediaType contentType = response.errorBody().contentType(); //
Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8;
charset = Util.readBomAsCharset(peekSource, charset);
// Use the line below in older versions of okhttp
// charset = Util.bomAwareCharset(peekSource, charset);
// Read string without consuming data from the original BufferedSource.
return peekSource.readString(charset);
}
And the necessary import statements:
import java.nio.charset.Charset;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.internal.Util;
import okhttp3.internal.http2.ConnectionShutdownException;
import okio.BufferedSource;
import retrofit2.Response;
Explanation:
As it was mentioned, you need to use response.errorBody().string()
only once. But there is a way to get the error body string more than once.
This is based on the original response.errorBody().string()
method implementation. It uses the copy of BufferedSource
from peek()
and returns the error body string without consuming it, so you can call it as many times as you need.
If you look at the response.errorBody().string()
method implementation, you'll see this:
public final String string() throws IOException {
try (BufferedSource source = source()) {
Charset charset = Util.bomAwareCharset(source, charset());
return source.readString(charset);
}
}
source.readString(charset)
consumes data of the error body's BufferedSource
instance, that's why response.errorBody().string()
returns an empty string on next calls.
To read from error body's BufferedSource
without consuming it we can use peek()
, which basically returns a copy of the original BufferedSource
:
Returns a new BufferedSource that can read data from this BufferedSource without consuming it.
Upvotes: 4
Reputation: 371
you can use Gson to get errorBody as your desired model class:
val errorResponse: ErrorMessage? = Gson().fromJson(
response.errorBody()!!.charStream(),
object : TypeToken<ErrorMessage>() {}.type
)
Upvotes: 0
Reputation: 11921
First create an Error class like below:
public class ApiError {
@SerializedName("httpStatus")
private int statusCode;
@SerializedName("errorMessage")
private String message;
public ApiError() {
}
public ApiError(String message) {
this.message = message;
}
public ApiError(int statusCode, String message) {
this.statusCode = statusCode;
this.message = message;
}
public int status() {
return statusCode;
}
public String message() {
return message;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
}
Second you can create a Utils class to handle your error like below:
public final class ErrorUtils {
private ErrorUtils() {
}
public static ApiError parseApiError(Response<?> response) {
final Converter<ResponseBody, ApiError> converter =
YourApiProvider.getInstance().getRetrofit()
.responseBodyConverter(ApiError.class, new Annotation[0]);
ApiError error;
try {
error = converter.convert(response.errorBody());
} catch (IOException e) {
error = new ApiError(0, "Unknown error"
}
return error;
}
And finally handle your error like below:
if (response.isSuccessful()) {
// Your response is successfull
callback.onSuccess();
}
else {
callback.onFail(ErrorUtils.parseApiError(response));
}
I hope this'll help you. Good luck.
Upvotes: -1
Reputation: 413
I encountered the same problem before and I fixed it by using the code response.errorBody().string() only once. You'll receive the IOException if you are using it many times so it is advised to just use it as a one-shot stream just as the Documentation on ResponseBody says.
My suggestion is: convert the Stringified errorBody() into an Object immediately as the latter is what you're gonna be using on subsequent operations.
Upvotes: 23
Reputation: 910
If you are gettig 400 then its a bad request you r trying to send to server. check your get req.
Upvotes: -3