Skip to content

Provide a standard exception hierarchy for REST end points [SPR-12531] #17136

@spring-projects-issues

Description

@spring-projects-issues

Adib Saikali opened SPR-12531 and commented

When building a single page application where the client side is storing the majority of state and server is mostly stateless there is a need for a consistent way to handle errors so that the client side javascript can know what to do.

In a JavaScript heavy app with 10K+ Lines of JS or more having every rest end point do it's own thing for errors can mean a huge mess in the client side JavaScript code base.

So most SpringMVC applications I have worked with in the past few years have built a standard way to handle errors so that.

  • JavaScript client knows exactly what to expect from the server when there is an error
  • Server side developers know exactly how to generate an error that the client side code and developers can consume

Benefits of a consistent error handling strategy

  • Server side APIs are easier to consume
  • Server side APIs are easier to code
  • Server side APIs are easier to design
  • It is easier to log and run analytics on API errors

Three part solution

  • Use HTTP status codes correctly
  • Define a standard data structure for representing errors
  • Make sure that no matter what happens API’s calls always return the standard data structure when an error occurs

Wikipedia lists 83 HTTP status codes so there is a lot of confusion as to what http status codes to use on the part of developers. So the idea to use a subset of HTTP status codes to reduce confusion.

There are 3 possible types of codes

  • Everything worked
  • Blame the API
  • Blame the client

Recommended HTTP Status Codes

  • 200 – if the request is processed successfully
  • 500 – if the problem is the server’s fault
  • 400 – if the request is the client’s fault
  • 401 – if the user needs to log in
  • 403 – if the user is logged in but does not have permission
  • 404 – If the resource does not exist

How do we provide the client with more details about the nature of the problem?

Use a secondary API specific error code

  • Complement the http status code with an optional API specific code
  • The API specific error code should be globally unique so that it is easy to search for the code. Use a UUID for the API specific error code

Five Part Standard Error Response

  • HTTP status code
    • Required and must be one of 200, 500, 400, 401, 403, 404
  • API specific error code
    • Optional must be a UUID so that is globally unique and searchable
  • User message
    • Optional plain text message that the UI can display to the user if it wants
  • Developer message
    • Optional plain text message aimed at developers to explain why the request failed
  • Validation messages
    • Optional array of field validation messages

Here are some example error messages that would be produced

GET: /api/customers/1
response:

{  
   "httpStatus":401,
   "apiCode":"cdbfa4c6-9ae6-46da-aef7-fa167f689afe",
   "userMessage":"Your account was signed out for your security, because of a lack of activity.",
   "developerMessage":"The server side http session has expired, user needs to login in again",
   "validationErrors":null
}

POST: /api/customers, {"name":"Jane","email":"[email protected]"}
response:

{  
   "httpStatus":400,
   "apiCode":"062952b8-257f-4fb5-9baf-2d2932375f9d",
   "userMessage":"An Error has occured and the request could not be completed",
   "developerMessage":"Invalid request body see validation errors",
   "validationErrors":[  
      {  
         "fieldName":"name",
         "message":"name must be 10 characters long"
      }
   ]
}

The git repo at https://bitbucket.org/asaikali/springone-2014/ contains a demo implementation of these features from my Talk at SpringOne 2014.

As a server side developer I want to be able to simply throw an exception that produces the standard error message for example.

@RequestMapping(ApiUrls.CUSTOMER)
	public CustomerJson get(@PathVariable("id") Integer id)
	{
		CustomerJson customerJson = this.customers.get(id);
		if(customerJson == null) {
			
			throw new ResourceNotFoundRestApiException()
					  .userMessage("User id %s does not exist",id)
					  .developerMessage("wrong user id the url");
		}
	    return customerJson;
	}

The implementation of this end user experience is located in the git repo at ttps://bitbucket.org/asaikali/springone-2014/ it is a spring boot application just clone it and run the SpaApplication then go to http://localhost:8080 and login using the username/password combo user/password and try out the features.

To best understand the code look at the classes in the following order:

  • com.example.rest.api.RestApiHttpStatus
  • com.example.rest.api.RestApiError
  • com.example.rest.api.RestApiValidationError
  • com.example.rest.api.RestApiException
  • com.example.rest.api.RestControllerAdvice
  • com.example.rest.api.ApiErrorCodes

Then look at the exception classes in the package com.example.rest.api.exceptions

To see what the end user experience is like look at the class com.example.spa.customer.CustomerJson and notice that it has an @Valid annotation on one of the fields then go to com.example.spa.customer.CustomerController and look at the post(@RequestBody @Valid CustomerJson customerJson) method and notice that the standard error message json gets generated if the posted json fails validation. Then look at get(@PathVariable("id") Integer id) to see how a developer will explicitly throw a standard error.

Given the opinionated nature of this request, maybe SpringBoot is a better home for it.


Affects: 4.1 GA

Attachments:

Issue Links:

Referenced from: commits spring-attic/spring-framework-issues@0cb6d39

2 votes, 7 watchers

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)status: declinedA suggestion or change that we don't feel we should currently applytype: enhancementA general enhancement

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions