Embracing the Messiness in Search of Epic Solutions

Backbone: model.destroy() Always Call “error” Callback Function

Posted

in

PROBLEM

Let’s assume we invoke person.destroy() to delete a person:-

var person = Backbone.Model.extend( { ... } );

person.destroy( {
    contentType : 'application/json',

    success : function () {
        console.log('success');
    },

    error : function () {
        console.log('error');
    }
} );

… and let’s assume when person.destroy() is invoked, it will call /person/{personId} DELETE. Here’s how the Spring MVC controller API might look like:-

@Controller
public class PersonController {
    ...

    @RequestMapping(value = "/person/{personId}", method = RequestMethod.DELETE)
    public ResponseEntity delete(@PathVariable Long personId) {

        Person person = personService.getPerson(personId);

        if (person == null) {
            return new ResponseEntity<UnexpectedErrorBean>(new UnexpectedErrorBean("Invalid person ID"),
                                                           HttpStatus.BAD_REQUEST);
        }

        personService.remove(person);

        return new ResponseEntity(HttpStatus.OK);
    }
}

When we execute person.destroy(), the error callback function will always get called even though the HTTP status is a 200 OK.

Why?

SOLUTION

The reason is because Backbone expects a JSON payload from the web service call. In the example above, we return a 200 OK but without a JSON payload. Thus, Backbone will treat this as an error and invoke the error callback function instead of success callback function regardless of the HTTP status.

There are three solutions to this problem.

Solution 1: Add a JSON Payload

@Controller
public class PersonController {
    ...

    @RequestMapping(value = "/person/{personId}", method = RequestMethod.DELETE)
    public ResponseEntity delete(@PathVariable Long personId) {

        Person person = personService.getPerson(personId);

        if (person == null) {
            return new ResponseEntity<UnexpectedErrorBean>(new UnexpectedErrorBean("Invalid person ID"),
                                                           HttpStatus.BAD_REQUEST);
        }

        personService.remove(person);

        return new ResponseEntity<Long>(personId, HttpStatus.OK);
    }
}

In this approach, we add a JSON payload to satisfy Backbone. In the above example, we return the value of personId back to the client. It can be an empty object too.

I don’t like this approach because that piece of information is useless to the client side. Even if I return an empty object, the solution seems very brittle because the next team member inheriting my code will not understand why there’s a need to return an empty object back to the client.

Solution 2: Accept Payload as Text

var person = Backbone.Model.extend( { ... } );

person.destroy( {
    contentType : 'application/json',

    dataType : 'text',

    success : function () {
        console.log('success');
    },

    error : function () {
        console.log('error');
    }
} );

Another approach is to accept the data as text from the server. This solution works fine if we are not expecting any complex data structure back from the server.

Solution 3: Return a Different HTTP Status

@Controller
public class PersonController {
    ...

    @RequestMapping(value = "/person/{personId}", method = RequestMethod.DELETE)
    public ResponseEntity delete(@PathVariable Long personId) {

        Person person = personService.getPerson(personId);

        if (person == null) {
            return new ResponseEntity<UnexpectedErrorBean>(new UnexpectedErrorBean("Invalid person ID"),
                                                           HttpStatus.BAD_REQUEST);
        }

        personService.remove(person);

        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}

The approach that I truly recommend is to return a 204 No Content instead of 200 OK. This way, the client side will not expect any payload from the server, and thus, Backbone will invoke the success callback function.

Comments

Leave a Reply