Jeremy Jeremy - 3 months ago 24
Android Question

Retrofit2: Error Handling

I am using an XML API which returns lists of users, customers, articles and so on:

<userlist access="ro">
<multipage>
<!-- Info about nums of pages, elements per page etc. -->
</multipage>
<user access="ro">
<surname access="rw" type="string">Jon</surname>
<lastname access="rw" type="string">Doe</lastname>
<scannerpin access="ro" type="int">1234</scannerpin>
</user>
<user>
<!-- ... -->
</user>
</userlist>


Currently, my POJO looks something like this:

@Root(name="userlist")
public class User extends XmlObject {
@Element
private XmlElement surname;

@Element
private XmlElement lastname;

@Element
private XmlElement scannerpin;
}

@Root(strict=false)
/* XXX no support for generics in SimpleXML. TODO Find better solution */
public class XmlUserList extends XmlList<User> {

/**
* Constructor for XmlUserList
*
* @param multipage
*/
public XmlUserList(@Element(name = "multipage") Multipage multipage) {
super(multipage);
}
}

public class XmlElement extends XmlNode {
protected static final String rcsid = "$Id: XmlElement.java 29660 2016-08-17 15:08:39Z jb $";

/**
* The element's value
*/
@Text(required=false)
String value;

/**
* Default constructor for XmlElement
*/
public XmlElement() {
}

/**
* Getter for value
* @return this.value
*/
public String getValue() {
return this.value != null ? this.value : "";
}

/**
* Setter for value
* @param value The new value
* @throws UnsupportedOperationException Iff this element is readOnly
*/
public void setValue(String value) throws UnsupportedOperationException {
this.value = value;
}

public void setValue(Integer value) {
this.value = String.valueOf(value);
}
}


public abstract class XmlList<T> extends XmlNode {
protected static final String rcsid = "$Id: XmlList.java 29660 2016-08-17 15:08:39Z jb $";

/**
* List entries
*/
@ElementListUnion({
@ElementList(inline = true, name = "user", type = User.class,required=false),
@ElementList(inline = true, name = "customer", type = Customer.class,required=false),
@ElementList(inline = true, name = "article", type = Article.class,required=false)
})
private List<T> list;

/**
* Multipage object
*/
private Multipage multipage;

/**
* Constructor for XmlList
*/
public XmlList(@Element(name="multipage") Multipage multipage) {
this.multipage = multipage;
}

/**
* getter for list
* @return this.list
*/
public List<T> getList() {
return (this.list);
}

public void setList(List<T> list) {
this.list = list;
}

/**
* Getter for Multipage
* @return this.multipage
*/
@Element(name="multipage")
public Multipage getMultipage() {
return (this.multipage);
}
}


On error (e.g. wrong login, downtime), the backend does not return a list, but an error message and sends HTTP 200, anway:

<error>
<message>Not logged in</message>
<softver>2.4.0_231445</softver>
</error>


I am wondering how to catch errors the right way here. I could add an optional field
message
to XmlObject, but this would mean that I had to annotate each and every field with
required=false
to prevent exceptions on error. I tried this approach, but rolled back due to dependency chaos (every list MUST have a multipage member, for example). I could check for this in a
@Commit
method, but there are several constraints that I would like to enforce, preferably using SimpleXML annotation.

How do you handle backend errors in Retrofit2 with SimpleXML converter?

Answer

I found a solution for this. The problem was that the backend returns HTTP/1.1 200 OK on error. I wrote an interceptor which checks if the response can be parsed into an ApiError object and, if so, overrides the response's HTTP status code to HTTP/1.1 400 Bad Request.

final class ErrorInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        Response originalResponse = chain.proceed(chain.request());
        Response.Builder responseBuilder = originalResponse.newBuilder();

        /* 
         * get body from response. caution! .string() cannot be called more 
         * than once, so a new response is built with the old response's body
         */
        String bodyString = originalResponse.body().string();

        // if response is an error: override response code to 400
        if (isError(bodyString)) {
            responseBuilder.code(400);
        }

        // clone response into new one
        responseBuilder.body(
            ResponseBody.create(originalResponse.body().contentType(),          
                                bodyString));

        return responseBuilder.build();
    }

    public boolean isError(String body) {
        Serializer ser = new Persister();

        try {
            return ser.validate(ErrorUtils.ApiError.class, body);
            // ser.validate() will throw if body can't be read into ApiError.class
        } catch (Exception e) {
            return false;
        }
    }
}

@Root(name="error")
public class ApiError {

    @Element
    private String message;

    @Element
    private String softver;

    public ApiError(@Element(name="message") String message, @Element(name="softver") String softver) {
        this.message = message;
        this.softver = softver;
    }

    public String getMessage() {
        return message;
    }

    public String getSoftver() {
        return softver;
    }
}