Error handling with REST

When I teach REST as part of the Enterprise Integration with Spring training, a question comes up quite often: how to deal with errors? People try to map what they’re used to doing with SOAP web services, and they don’t see the equivalent in REST. This post covers the basics of error handling in REST and shows how to implement them with Spring MVC.

Everything is in the HTTP response…

When a client sends a request to a HTTP server, the server answers with… a HTTP response. A HTTP response is made of the following:

  • a status line
  • HTTP headers
  • content (the body)

Today, we’re interested in the status line.

What is the status line in a HTTP response?

The status line should be enough for the client to know how its request was processed (or at least to have an idea). This is an example of a status line:

HTTP/1.1 200 OK

There are three elements in the status line:

  • the version of HTTP used
  • the status code
  • a short description of the status code

Believe it or not, but this is enough to deal with errors in REST… well, almost. The trick is to know which status code to use when responding to the client.

Status codes at the rescue

So the status code of a HTTP response should bring enough semantics about the processing of the request: was the processing successful? Did the client do something wrong? Did the server do something wrong? Luckily, status codes are standardized and the average web developer knows some of them: 404 (resource not found), 200 (everything went fine), etc.
HTTP status codes are organized in series:

  • informational (100-199)
  • successful (200-299)
  • redirection (300-399)
  • client error (400-499)
  • server error (500-599)

You can find all the standard status codes here.

Which status code to use?

Well, it depends on what happened! We focus on error in this post, so we’re more interested in the 400 and 500 series. But as the 500 series is about server errors (bugs or service unavailability), a developer of REST web services cares mainly about the 400 series (when writing your service, you can’t really know about gateways or bandwidth limitations, this is the server infrastructure’s job, not yours). Ok, let’s see a couple of examples.

The dreaded 404

A service should send back a 404 when the client asked for a resource that doesn’t exist. Perhaps the client asks for a resources with a given ID, so there’s no row in the database with this ID, so 404! That’s the example we’ll implement with Spring MVC later.

The rigid 405

The usual short description of 405 is Method Not Allowed. Imagine an immutable resource, like an order that has been confirmed, paid, shipped, and received. This resource can’t change anymore: it reached the end of its workflow. If a client sends a PUT request – PUT equals to update in REST – the service should then answer with a 405.

The rich 409

The short description for 409 is Conflict. There are many use cases where 409 is an appropriate status code:

  • optimistic locking: a client GET a resource, change its state locally, and sends a PUT to update the resource. Unfortunately, another client updated the resource between the GET and the PUT. If the server can detect the conflict – thanks to a timestamp on the resource or a version number – the appropriate response would be a 409.
  • pessimistic locking: a resource is accessible only by one client at a time, other clients trying to GET the resource could get a 409 until the lock is released (with a PUT for example or after a timeout). Note the 423 status code also holds the semantics of pessimistic locking.
  • imagine clients submitting long-running tasks to be processed in the background by the server. Each task would have a client-provided ID. If a client submits a task with the same ID twice, the server could answer with a 409.

All the standard status codes (up to 426) provide quite rich semantics to inform clients about an error. You can also imagine your own status codes, but you’ll need to inform your clients about their meaning.

And what about the short description?

A status code comes with a short description. Standard status codes have a pre-defined short description, but you can change it if you want to provide more specific information.

Don’t forget headers

Headers are another element of the response, and they can’t complement the status code. When responding with 405 Method Not Allowed, the response can contain an Allow header to list the methods the resource supports.

And the content!

A response can also contain some content to describe even more the error. You’re not limited to a status code and a short description. Take care the client actually understands the content you’re sending back (plain text, HTML, etc.)

Status codes and error handling in action with Spring MVC

Spring MVC provides REST support as of Spring 3.0. You can implement all the REST of the planet with the plain servlet API, but a framework like Spring MVC is here to help you develop REST by using common patterns, like exception handling. Let’s see how to do that on the server side, before moving on to the client side.

A RESTful web controller

Our web controller is quite simple: it returns a contact resource. The client must provide the ID of the contact by GETting a URL like /contacts/123 (123 being the ID).

200 when everything goes fine

Here is the code for the Spring MVC controller:

package com.zenika.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import com.zenika.domain.Contact;
import com.zenika.repo.ContactRepository;
@Controller
public class ContactController {
  @Autowired
  private ContactRepository contactRepository;
  @RequestMapping(value="/contacts/{id}",method=RequestMethod.GET)
  @ResponseStatus(HttpStatus.OK)
  public @ResponseBody Contact get(@PathVariable Long id) {
    return contactRepository.get(id);
  }
}

Spring MVC heavily relies on annotations for the configuration of controllers. The key point here is the ResponseStatus annotation, that tells Spring MVC which status code to send back. But what happe
ns if there’s no contact for the given ID?

Bad ID, bad response?

The controller relies on a ContactRepository to load the Contacts. The contract of the ContactRepository is to throw a EmptyResultDataAccessException when there’s no contact for the given ID. In this case, Spring MVC’s default is to send back a 500 status code, which is bad for this specific error. Let’s see how to send a 404 when the requested resource doesn’t exist.

404 when the resource doesn’t exist

Whenever a controller method throws an exception, Spring MVC can fall back to an exception handler method. Such a method is declared by annotating the method with the ExceptionHandler annotation. Here is the modified version of the controller with exception handler method when EmptyResultDataAccessException is thrown.

package com.zenika.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import com.zenika.domain.Contact;
import com.zenika.repo.ContactRepository;
@Controller
public class ContactController {
  @Autowired
  private ContactRepository contactRepository;
  @RequestMapping(value="/contacts/{id}",method=RequestMethod.GET)
  @ResponseStatus(HttpStatus.OK)
  public @ResponseBody Contact get(@PathVariable Long id) {
    return contactRepository.get(id);
  }
  @ExceptionHandler(EmptyResultDataAccessException.class)
  @ResponseStatus(value=HttpStatus.NOT_FOUND,reason="Contact not found")
  public void notFound() { }
}

How does it work?

Spring MVC calls the controller’s get method

the controller calls the ContactRepository that throws a EmptyResultDataAccessException because there’s no contact for the given ID

Spring MVC catches the exception and see there’s an exception handler method for this kind of exceptions (see the value in the ExceptionHandler annotation on the notFound method)

Spring MVC calls the exception handler method

the exception handler method does nothing apart from providing the status code and the short description to send back.

Note the notFound method customizes the short description: Contact not found.
The point here is to show how a framework like Spring MVC helps to implement some REST practices. Spring MVC uses annotations in this case. But what about the client? Must it analyze the status code to know if its request has been processed correctly?

Error handling on the client side

A REST client must always analyze the status code, but again, a framework can help here. A good example is Spring MVC’s RestTemplate. By default, the RestTemplate throws an exception for any response in the 400 or 500 series. The developer can customize this by providing its own implementation of ResponseErrorHandler. The following listing shows a JUnit test that shows a successful GET and a GET that ends up with a 404. The developer must handle the later in a catch block.

public class RestErrorHandlingTest {
  private String url = "http://localhost:8081/rest/contacts/{id}";
  private RestTemplate restTemplate = new RestTemplate();
  @Test public void getContactOk() {
    Contact contact = restTemplate.getForObject(url, Contact.class, "1");
    assertNotNull(contact);
    assertEquals(1L, contact.getId().longValue());
  }
  @Test public void getContactNotFound() {
    try {
      restTemplate.getForObject(url, Contact.class, "2");
      fail("no contact with this id");
    } catch (HttpClientErrorException e) {
      assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode());
    } catch(Exception e) {
      fail("this isn't the expected exception: "+e.getMessage());
    }
  }
}

The default error handling behavior makes sense, as it prevents the developer from checking the status code after each request. The developer can let an exception interrupt the execution flow, as usual.

Summary

Hopefully, the way to deal with errors in typical REST services is clearer to you now. There’s no magic: this is plain HTTP and you need to carefully choose the status codes to send back to the clients. Frameworks like Spring MVC can help to stick to the common patterns you have in the your favorite language, like exceptions.
The source code for this post is available on Zenika’s GitHub repo.


Learning more :

Une pensée sur “Error handling with REST

  • 12 septembre 2011 à 13 h 34 min
    Permalink

    Hi,
    Your post is very helpful. I was looking for help on reading the response headers on error 400. Any help is appreciated.
    I have a rest service which will return response 400 and more details about the response in the response headers. The header name is « BadFields » (a custom header). When I make a call using rest template, after I get the 400 response I can’t get the response headers from it.

    I’m running the following code:
    try {

    HttpEntity<String> response = template.exchange(url, HttpMethod.POST, request, String.class);

    String resultString = response.getBody();
    HttpHeaders headers = response.getHeaders();
    } catch (HttpClientErrorException e) {
    // how do I get the response headers here?
    }
    Thanks,
    Rasin

    Répondre

Répondre à Rasin Annuler la réponse.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :