Retrofit 2.0 cómo obtener una respuesta de error deserializada.

129

Estoy usando Retrofit 2.0.0-beta1 .

En las pruebas tengo un escenario alternativo y espero el error HTTP 400

Me gustaria tener retrofit.Response<MyError> response masresponse.body() == null

MyError no está deserializado, solo lo veo aquí

response.errorBody().string()

pero no me da MyError como objeto

Piotr Boho
fuente
¿Es una buena práctica deserializar la respuesta de error? ya que la respuesta podría ser un error del servidor web que es html.
Hossein Shahdoost
1
Gracias @ahmadalibaloch, ese enlace es realmente muy útil.
Ravi Vaniya

Respuestas:

138

Actualmente uso una implementación muy fácil, que no requiere el uso de convertidores o clases especiales. El código que uso es el siguiente:

public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
    DialogHelper.dismiss();

    if (response.isSuccessful()) {
        // Do your success stuff...
    } else {
        try {
            JSONObject jObjError = new JSONObject(response.errorBody().string());
            Toast.makeText(getContext(), jObjError.getJSONObject("error").getString("message"), Toast.LENGTH_LONG).show();
        } catch (Exception e) {
            Toast.makeText(getContext(), e.getMessage(), Toast.LENGTH_LONG).show();
        }
    }
}
Saif Bechan
fuente
66
Su solución no muestra un contenido de respuesta de error.
CoolMind
1
Comprueba mi edición, no sé por qué lo hice tan poco claro en primer lugar.
Saif Bechan
44
Finalmente, una respuesta simple a una pregunta de Android que funciona (la mayoría de las respuestas de Android son cómicamente complejas).
Doug Voss
Esto definitivamente no responde la pregunta. Simplemente devuelve el mensaje de error y no el objeto Error Enum. Siga esta respuesta: stackoverflow.com/a/21103420/2914140
Tobliug
En la respuesta está buscando una asignación a "mensaje", pero mi respuesta de error no tenía eso. Tenía una asignación a "error". Así que todos los que lean, ¡depende de la respuesta que obtengas!
mco
43

ErrorResponse es su objeto de respuesta personalizado

Kotlin

val gson = Gson()
val type = object : TypeToken<ErrorResponse>() {}.type
var errorResponse: ErrorResponse? = gson.fromJson(response.errorBody()!!.charStream(), type)

Java

Gson gson = new Gson();
Type type = new TypeToken<ErrorResponse>() {}.getType();
ErrorResponse errorResponse = gson.fromJson(response.errorBody.charStream(),type);
Shahab Rauf
fuente
3
no deberías forzar el desenvolvimiento de las opciones:gson.fromJson(response.errorBody()?.charStream(), type)
hopeman
37

Lo resolví por:

 if(!response.isSuccessful()){
       Gson gson = new Gson();
       MyErrorMessage message=gson.fromJson(response.errorBody().charStream(),MyErrorMessage.class);
       if(message.getCode()==ErrorCode.DUPLICATE_EMAIL_ID_CODE){
                  //DO Error Code specific handling                        
        }else{
                 //DO GENERAL Error Code Specific handling                               
        }
    }

Clase MyErrorMessage:

  public class MyErrorMessage {
     private int code;
     private String message;

     public int getCode() {
        return code;
     }

     public void setCode(int code) {
        this.code = code;
     }

     public String getMessage() {
         return message;
     }

     public void setMessage(String message) {
        this.message = message;
     }
   }
Pooja Gupta
fuente
2
java.lang.IllegalStateException: se esperaba BEGIN_OBJECT pero estaba STRING en la línea 1 columna 2 ruta $
Ronel Gonzales
Use .addConverterFactory(ScalarsConverterFactory.create())@RonelGonzales
Pratik Butani
30

En Retrofit 2.0 beta2 esta es la forma en que obtengo respuestas de error:

  1. Sincrónico

    try {
       Call<RegistrationResponse> call = backendServiceApi.register(data.in.account, data.in.password,
               data.in.email);
       Response<RegistrationResponse> response = call.execute();
       if (response != null && !response.isSuccess() && response.errorBody() != null) {
           Converter<ResponseBody, BasicResponse> errorConverter =
                   MyApplication.getRestClient().getRetrofitInstance().responseConverter(BasicResponse.class, new Annotation[0]);
           BasicResponse error = errorConverter.convert(response.errorBody());
           //DO ERROR HANDLING HERE
           return;
       }
       RegistrationResponse registrationResponse = response.body();
       //DO SUCCESS HANDLING HERE
    } catch (IOException e) {
       //DO NETWORK ERROR HANDLING HERE
    }
  2. Asincrónico

    Call<BasicResponse> call = service.loadRepo();
    call.enqueue(new Callback<BasicResponse>() {
        @Override
        public void onResponse(Response<BasicResponse> response, Retrofit retrofit) {
            if (response != null && !response.isSuccess() && response.errorBody() != null) {
                Converter<ResponseBody, BasicResponse> errorConverter =
                    retrofit.responseConverter(BasicResponse.class, new Annotation[0]);
                BasicResponse error = errorConverter.convert(response.errorBody());
                //DO ERROR HANDLING HERE
                return;
            }
            RegistrationResponse registrationResponse = response.body();
            //DO SUCCESS HANDLING HERE
        }
    
        @Override
        public void onFailure(Throwable t) {
            //DO NETWORK ERROR HANDLING HERE
        }
    });

Actualización para Retrofit 2 beta3:

  1. Sincrónico - no cambiado
  2. Asíncrono: el parámetro de modificación se eliminó de onResponse

    Call<BasicResponse> call = service.loadRepo();
    call.enqueue(new Callback<BasicResponse>() {
        @Override
        public void onResponse(Response<BasicResponse> response) {
            if (response != null && !response.isSuccess() && response.errorBody() != null) {
                Converter<ResponseBody, BasicResponse> errorConverter =
                    MyApplication.getRestClient().getRetrofitInstance().responseConverter(BasicResponse.class, new Annotation[0]);
                BasicResponse error = errorConverter.convert(response.errorBody());
                //DO ERROR HANDLING HERE
                return;
            }
            RegistrationResponse registrationResponse = response.body();
            //DO SUCCESS HANDLING HERE
        }
    
        @Override
        public void onFailure(Throwable t) {
            //DO NETWORK ERROR HANDLING HERE
        }
    });
JFreeman
fuente
44
¿Qué tienes en BasicResponse?
Jemshit Iskenderov
2
Solo una clase anotada Jackson básica que contiene un mensaje y un código de error. En cualquier caso, puede tener allí cualquier clase anotada que coincida con el tipo de respuesta de su servidor. Intente usar jsonschema2pojo para generar uno que coincida con sus necesidades.
JFreeman
Para otros, puede usar esto en su lugar: Convertidor <ResponseBody, <Message> errorConverter = retrofit.responseBodyConverter (Message.class, new Annotation [0]);
Kim Montano
@JFreeman, ¿qué pasa si quiero deserializar List<BasicResponse>?
azizbekian
puedes ver la clase MyApplication
dungtv
10

En https://stackoverflow.com/a/21103420/2914140 y https://futurestud.io/tutorials/retrofit-2-simple-error-handling, esta variante se muestra para Retrofit 2.1.0.

call.enqueue(new Callback<MyResponse>() {
    @Override
    public void onResponse(Call<MyResponse> call, Response<MyResponse> response) {
        if (response.isSuccessful()) {
            ...
        } else {
            Converter<ResponseBody, MyError> converter
                    = MyApplication.getRetrofit().responseBodyConverter(
                    MyError.class, new Annotation[0]);
            MyError errorResponse = null;
            try {
                errorResponse = converter.convert(response.errorBody());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
CoolMind
fuente
10

Cree un modelo de respuesta de error y usuario Gson para convertir la respuesta a él. Esto simplemente funcionará bien.

APIError.java

public class APIError {
    private String message;

    public String getMessage() {
        return message;
    }
}

MainActivity.java (solicitud interna en respuesta)

if (response.isSuccessful()) {
    // Do your success stuff...

} else {
    APIError message = new Gson().fromJson(response.errorBody().charStream(), APIError.class);
    Toast.makeText(MainActivity.this, "" + message.getMessage(), Toast.LENGTH_SHORT).show();
}
Sreekant Shenoy
fuente
9
 @Override
 public void onResponse(Call<Void> call, retrofit2.Response<Void> response) {
            if (response.isSuccessful()) {

            //Do something if response is ok
            } else {

                JsonParser parser = new JsonParser();
                JsonElement mJson = null;
                try {
                    mJson = parser.parse(response.errorBody().string());
                    Gson gson = new Gson();
                    MyError errorResponse = gson.fromJson(mJson, MyError.class);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }

            }
Vins
fuente
7

Lo hice de esta manera para llamadas asíncronas usando Retrofit 2.0-beta2:

@Override
public void onResponse(Response<RegistrationResponse> response, 
                       Retrofit retrofit) {
    if (response.isSuccess()) {
        // Do success handling here
    } else {
        try {
            MyError myError = (MyError)retrofit.responseConverter(
                    MyError.class, MyError.class.getAnnotations())
                .convert(response.errorBody());
            // Do error handling here
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
shantanu
fuente
¿Cuál será la clase MyError?
Dhrupal
Pensé que se suponía que onResponse debía contener un parámetro Call y un parámetro Response. ¿Cómo es que el tuyo tiene un parámetro Retrofit?
Marty Miller
@MartyMiller esto se hizo para la siguiente versión de retrofit Retrofit 2.0-beta2
shantanu
7

Si usa Kotlin, otra solución podría ser crear una función de extensión para la clase Response:

inline fun <reified T>Response<*>.parseErrJsonResponse(): T?
{
    val moshi = MyCustomMoshiBuilder().build()
    val parser = moshi.adapter(T::class.java)
    val response = errorBody()?.string()
    if(response != null)
        try {
            return parser.fromJson(response)
        } catch(e: JsonDataException) {
            e.printStackTrace()
        }
    return null
}

Uso

val myError = response.parseErrJsonResponse<MyErrorResponse>()
if(myError != null) {
   // handle your error logic here
   // ...
}
Arsenio
fuente
¡Finalmente alguien usó el poder de Kotlin para hacer que el código sea fácil de leer!
Vince
6

En realidad es muy sencillo.

Kotlin:

val jsonObj = JSONObject(response.errorBody()!!.charStream().readText())
responseInterface.onFailure(jsonObj.getString("msg"))

Java:

JSONObject jsonObj = new JSONObject(response.errorBody().charStream().readText());
responseInterface.onFailure(jsonObj.getString("msg"));

Probado en la actualización: 2.5.0. Lea el texto de charStream que le dará una Cadena, luego analice a JSONObject.

Adios

Whales_Corps
fuente
no hay readText()extensión en java, TextStreamsKt.readText(response.errorBody().charStream())
úsela
4

Estaba enfrentando el mismo problema. Lo resolví con la modificación. Déjame mostrarte esto ...

Si su estructura de error JSON es como

{
"error": {
    "status": "The email field is required."
}
}


My ErrorRespnce.java 

public class ErrorResponse {

   @SerializedName("error")
   @Expose
   private ErrorStatus error;

   public ErrorStatus getError() {
      return error;
   }

   public void setError(ErrorStatus error) {
      this.error = error;
   }
}

Y esta es mi clase de estado de error

public class ErrorStatus {

  @SerializedName("status")
  @Expose
  private String status;

  public String getStatus() {
      return status;
  }

  public void setStatus(String status) {
      this.status = status;
  }
}

Ahora necesitamos una clase que pueda manejar nuestro json.

  public class ErrorUtils {

   public static ErrorResponse parseError (Response<?> response){
      Converter<ResponseBody , ErrorResponse> converter =          ApiClient.getClient().responseBodyConverter(ErrorResponse.class , new Annotation[0]);
    ErrorResponse errorResponse;
    try{
        errorResponse = converter.convert(response.errorBody());
    }catch (IOException e){
        return new ErrorResponse();
    }
    return errorResponse;
}
}

Ahora podemos verificar nuestra respuesta en la actualización de la llamada API

private void registrationRequest(String name , String email , String password , String c_password){


    final Call<RegistrationResponce> registrationResponceCall = apiInterface.getRegistration(name , email , password , c_password);
    registrationResponceCall.enqueue(new Callback<RegistrationResponce>() {
        @Override
        public void onResponse(Call<RegistrationResponce> call, Response<RegistrationResponce> response) {



            if (response.code() == 200){


            }else if (response.code() == 401){


                ErrorResponse errorResponse = ErrorUtils.parseError(response);
                Toast.makeText(MainActivity.this, ""+errorResponse.getError().getStatus(), Toast.LENGTH_SHORT).show();
            }
        }

        @Override
        public void onFailure(Call<RegistrationResponce> call, Throwable t) {

        }
    });
}

Eso es todo, ahora puedes mostrar tu brindis

pavel
fuente
4

Aquí hay una solución elegante usando Kotlinextensiones:

data class ApiError(val code: Int, val message: String?) {
    companion object {
        val EMPTY_API_ERROR = ApiError(-1, null)
    }
}

fun Throwable.getApiError(): ApiError? {
    if (this is HttpException) {
        try {
            val errorJsonString = this.response()?.errorBody()?.string()
            return Gson().fromJson(errorJsonString, ApiError::class.java)
        } catch (exception: Exception) {
            // Ignore
        }
    }
    return EMPTY_API_ERROR
}

y uso:

showError(retrofitThrowable.getApiError()?.message)

Antonis Radz
fuente
3

De esta manera, no necesita una instancia de Retrofit si solo está inyectando un servicio creado a partir de Retrofit.

public class ErrorUtils {

  public static APIError parseError(Context context, Response<?> response) {

    APIError error = new APIError();

    try {
        Gson gson = new Gson();
        error = gson.fromJson(response.errorBody().charStream(), APIError.class);
    } catch (Exception e) {
        Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show();
    }

    if (TextUtils.isEmpty(error.getErrorMessage())) {
        error.setError(response.raw().message());
    }
    return error;
  }
}

Úselo así:

if (response.isSuccessful()) {

      ...

    } else {

      String msg = ErrorUtils.parseError(fragment.getActivity(), response).getError(); // would be from your error class
      Snackbar.make(someview, msg, Snackbar.LENGTH_LONG).show();
    }
  }
Codeversed
fuente
3

Leer errorBody en una cadena y analizar manualmente json.

 if(!response.isSuccessful()) {
   
    String error = "";
    try {
        BufferedReader ereader = new BufferedReader(new InputStreamReader(
                response.errorBody().byteStream()));
        String eline = null;
        while ((eline = ereader.readLine()) != null) {
            error += eline + "";
        }
        ereader.close();
    } catch (Exception e) {
        error += e.getMessage();
    }
    Log.e("Error",error);
    
    try {
        JSONObject reader = new JSONObject(error);
        String message = reader.getString("message");
    
        Toast.makeText(context,message,Toast.LENGTH_SHORT).show();
    
    } catch (JSONException e) {
        e.printStackTrace();
    }
 }
Riyas PK
fuente
2

Este parece ser el problema cuando usa OkHttp junto con Retrofit, por lo que puede eliminar OkHttp o usar el código a continuación para obtener el cuerpo del error:

if (!response.isSuccessful()) {
 InputStream i = response.errorBody().byteStream();
 BufferedReader r = new BufferedReader(new InputStreamReader(i));
 StringBuilder errorResult = new StringBuilder();
 String line;
 try {
   while ((line = r.readLine()) != null) {
   errorResult.append(line).append('\n');
   }
 } catch (IOException e) { 
    e.printStackTrace(); 
}
}
KRUPEN GHETIYA
fuente
0

resuelto por:

Converter<MyError> converter = 
    (Converter<MyError>)JacksonConverterFactory.create().get(MyError.class);
MyError myError =  converter.fromBody(response.errorBody());
Piotr Boho
fuente
¿Cómo puedo convertir a través de GsonConverterFactory? Alguna idea ?
Shan Xeeshi
Encontre un camino. Acabo de cambiar JacksonConverterFactory a GsonConverterFactoryConvierte JSON en mi objeto personalizado pero da aviso sin control retrofit.Converter fundido <captura <>>?
Shan Xeeshi
3
¿Qué clase de MyError tiene?
Dhrupal
0
try{
                ResponseBody response = ((HttpException) t).response().errorBody();
                JSONObject json = new JSONObject( new String(response.bytes()) );
                errMsg = json.getString("message");
            }catch(JSONException e){
                return t.getMessage();
            }
            catch(IOException e){
                return t.getMessage();
            }
Mike6679
fuente
0

En Kotlin:

val call = APIClient.getInstance().signIn(AuthRequestWrapper(AuthRequest("1234567890z", "12341234", "nonce")))
call.enqueue(object : Callback<AuthResponse> {
    override fun onResponse(call: Call<AuthResponse>, response: Response<AuthResponse>) {
        if (response.isSuccessful) {

        } else {
            val a = object : Annotation{}
            val errorConverter = RentalGeekClient.getRetrofitInstance().responseBodyConverter<AuthFailureResponse>(AuthFailureResponse::class.java, arrayOf(a))
            val authFailureResponse = errorConverter.convert(response.errorBody())
        }
    }

    override fun onFailure(call: Call<AuthResponse>, t: Throwable) {
    }
})
Adam Johns
fuente
0

Los valores de errorBody deben establecer el objeto APIError en Retrofit. Para que pueda usar la siguiente estructura de código.

public class APIErrorUtils {

    public static APIError parseError(Response<?> response) {
        Converter<ResponseBody, APIError> converter = API.getClient().responseBodyConverter(APIError.class, new Annotation[0]);

        APIError error;

        try {
            error = converter.convert(response.errorBody());
            Log.d("SERVICELOG", "****************************************************");
            Log.d("SERVICELOG", "***** SERVICE LOG");
            Log.d("SERVICELOG", "***** TIMESTAMP: " + String.valueOf(error.getTimestamp()));
            Log.d("SERVICELOG", "***** STATUS: " + String.valueOf(error.getStatus()));
            Log.d("SERVICELOG", "***** ERROR: " + error.getError());
            Log.d("SERVICELOG", "***** MESSAGE: " + error.getMessage());
            Log.d("SERVICELOG", "***** PATH: " + error.getPath());
            Log.d("SERVICELOG", "****************************************************");
        } catch (IOException e) {
            return new APIError();
        }

        return error;
    }
}

APIError error = APIErrorUtils.parseError(response);
if (error.getStatus() == 400) {
    ....
}
Egemen Mede
fuente
0

Probado y funciona

 public BaseModel parse(Response<BaseModel> response , Retrofit retrofit){
            BaseModel error = null;
            Converter<ResponseBody, BaseModel> errorConverter =
                    retrofit.responseBodyConverter(BaseModel.class, new Annotation[0]);
            try {
                if (response.errorBody() != null) {
                    error = errorConverter.convert(response.errorBody());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return error;
        }
El MJ
fuente
-1
val error = JSONObject(callApi.errorBody()?.string() as String)
            CustomResult.OnError(CustomNotFoundError(userMessage = error["userMessage"] as String))

open class CustomError (
    val traceId: String? = null,
    val errorCode: String? = null,
    val systemMessage: String? = null,
    val userMessage: String? = null,
    val cause: Throwable? = null
)

open class ErrorThrowable(
    private val traceId: String? = null,
    private val errorCode: String? = null,
    private val systemMessage: String? = null,
    private val userMessage: String? = null,
    override val cause: Throwable? = null
) : Throwable(userMessage, cause) {
    fun toError(): CustomError = CustomError(traceId, errorCode, systemMessage, userMessage, cause)
}


class NetworkError(traceId: String? = null, errorCode: String? = null, systemMessage: String? = null, userMessage: String? = null, cause: Throwable? = null):
    CustomError(traceId, errorCode, systemMessage, userMessage?: "Usted no tiene conexión a internet, active los datos", cause)

class HttpError(traceId: String? = null, errorCode: String? = null, systemMessage: String? = null, userMessage: String? = null, cause: Throwable? = null):
    CustomError(traceId, errorCode, systemMessage, userMessage, cause)

class UnknownError(traceId: String? = null, errorCode: String? = null, systemMessage: String? = null, userMessage: String? = null, cause: Throwable? = null):
    CustomError(traceId, errorCode, systemMessage, userMessage?: "Unknown error", cause)

class CustomNotFoundError(traceId: String? = null, errorCode: String? = null, systemMessage: String? = null, userMessage: String? = null, cause: Throwable? = null):
    CustomError(traceId, errorCode, systemMessage, userMessage?: "Data not found", cause)`
Gary Loyola
fuente