12

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

adamacdo
  • 426
  • 1
  • 6
  • 14
  • can you post your example code trying to access the body – snkashis Jul 14 '16 at 20:39
  • Have you found a solution to this problem? I'm struggling with the same issue, I can see en error response body in logs but errorBody content is empty. – Greg Aug 17 '16 at 13:58
  • I have not. I'm just handling any error from my API in a generic sense and not showing the meaningful error to the user. It's not ideal, but the only option I see before I understand why Retrofit / OkHttp are stripping the error info. – adamacdo Aug 17 '16 at 15:51
  • adamacdo, did you resolve your problem? I have the same issue.. Thank you very much – anthony Nov 04 '16 at 19:17
  • I have not. It was for a project I have since moved on from. I showed a generic error without the actual error from the server as a workaround. Not helpful, but literally better than nothing. – adamacdo Nov 04 '16 at 22:16

5 Answers5

20

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.

ReGaSLZR
  • 383
  • 4
  • 17
  • If you need to use response.errorBody().string() more than once, check this answer out: https://stackoverflow.com/a/73857446/9952567 – Vladimir Kattsyn Sep 26 '22 at 17:09
3

TL;DR 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.

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.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.

Vladimir Kattsyn
  • 606
  • 1
  • 4
  • 16
  • thank you for sharing the answer, But I have a problem with the Util.bomAwareCharset(peekSource, charset) function, Where are we getting it from? I mean from which library? – providerZ Aug 22 '23 at 05:37
  • @providerZ it is from okhttp3.internal.Util. Thanks for pointing that out, I will include it in the answer – Vladimir Kattsyn Aug 22 '23 at 12:43
  • Thank you Sir for the replay , but still Util is not exist in okhttp3.internal , maybe because of the library's version? , I tried couple of versions with negative result – providerZ Aug 23 '23 at 04:23
  • @providerZ In my code Util is from com.squareup.okhttp3:okhttp:3.14.9. I believe it is coming from this dependency: "com.squareup.retrofit2:retrofit:2.9.0" – Vladimir Kattsyn Aug 28 '23 at 14:08
0

you can use Gson to get errorBody as your desired model class:

val errorResponse: ErrorMessage? = Gson().fromJson(
                response.errorBody()!!.charStream(),
                object : TypeToken<ErrorMessage>() {}.type
            )
Sep
  • 147
  • 8
-1

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.

savepopulation
  • 11,736
  • 4
  • 55
  • 80
-3

If you are gettig 400 then its a bad request you r trying to send to server. check your get req.

  • That makes sense, but I would like to pull the error body text from the returned error to prompt to my user. When I get a 500 level error I see there is error body text returned as well. I would like to show any error body text regardless of the error, even if the error is due to an improperly formatted request. – adamacdo Jul 28 '16 at 17:06