Providing error response in RESTful API. Part 3


In the previous article we discussed error response for each specific situation. Now I’ll show you how this could be easily implemented in a Spring web application.

Convert Exception to HTTP response

We have a web layer where our controllers reside and a service layer where business logic runs. When our service runs into an exceptional situation it throws an exception which should be converted to a proper error response returned by web layer. At the same time we want to separate the concepts so that service layer does not operate with http response or status codes directly.

Spring offers a convenient solution for that — ExceptionHandler

Let’s take an example use case — a user submits a new contact form providing a companyId the contact should be tied to but our service cannot find that company in the DB. This time we should return a 400 Bad Request.

@Service
class ContactService(
  val companyRepository: CompanyRepository
) {
  fun createContact(name: String, companyId: Int) {
    val company = companyRepository.findById(companyId)
      .orElseThrow { IllegalArgumentException("Company not found: ID=$companyId") }
    ... // Save new contact 
  }
}

This service method is just throwing an exception, let’s handle it within the web layer. That could be performed with @ControllerAdvice and @ExceptionHandler annotations.

@ControllerAdvice
class RestAPIExceptionHandler {
  ...

  @ExceptionHandler(IllegalArgumentException::class)
  fun handleIllegalArgumentException(e: IllegalArgumentException, request: WebRequest) {
    val errorResponse = ErrorResponse("invalid_request", e.message)

    return ResponseEntity(errorResponse, HttpHeaders(), HttpStatus.BAD_REQUEST)
  }

  ...
}

Where ErrorResponse is a DTO for response body holding error code, message, solution and other information a client can use.

class ErrorResponse(
  val code: String,
  val message: String,
  val solution: String?
)

This way any time we throw an IllegalArgumentException in the service, data or web layer it is intercepted by RestAPIExceptionHandler and returned to client as an HTTP response with 400 Bad Request status code, specific "invalid_request" application code and a user friendly message.

Provide error specific data

"invalid_request" error code is too broad. For a client it would be more convenient if he knows which form field was incorrect and which value.

For that it makes sense to inherit specific responses from a generic ErrorResponse.

abstract class ErrorResponse(
  val code: String,
  val message: String,
  val solution: String?
)

open class InvalidRequestResponse(message: String): 
  ErrorResponse(
    code = "invalid_request",
    message = message
  )

class CompanyNotFoundResponse(val companyId: Int):
  ErrorResponse(
    code = "company_not_found",
    message ="Company not found: ID=$companyId",
    solution = "Provide another ID or create a new company"
  )

Note that we also provide companyId property within the specific response.

To return such response we need to also create a specific exception and a handler.

class CompanyNotFoundException(val companyId: Int):
  RuntimeException("Company not found: ID=$companyId")
@ControllerAdvice
class RestAPIExceptionHandler {
  ...

  @ExceptionHandler(CompanyNotFoundException::class)
  fun handleCompanyNotFoundException(e: CompanyNotFoundException, request: WebRequest) {
    val errorResponse = CompanyNotFoundReponse(e.companyId)

    return ResponseEntity(errorResponse, HttpHeaders(), HttpStatus.BAD_REQUEST)
  }

  ...
}

Return 404 instead of 400

If a company is requested as a resource by a direct URL we should return 404 instead. I suggest having additional exception and response classes for those cases.

class CompanyResourceNotFoundResponse(val companyId: Int):
  ErrorResponse(
    code = "company_not_found",
    message ="Company not found: ID=$companyId",
    solution = "Provide another ID or create a new company"
  )
class CompanyResourceNotFoundException(val companyId: Int):
  RuntimeException("Company not found: ID=$companyId")
@ControllerAdvice
class RestAPIExceptionHandler {
  ...

  @ExceptionHandler(CompanyResourceNotFoundException::class)
  fun handleCompanyResourceNotFoundException(e: CompanyResourceNotFoundException, request: WebRequest) {
    val errorResponse = CompanyResourceNotFoundReponse(e.companyId)

    return ResponseEntity(errorResponse, HttpHeaders(),   HttpStatus.NOT_FOUND)
  }

  ...
}

To keep things simple you can create generic classes for this two types of errors and extend them for specific types:

abstract class EntityNotFoundException(
  val entityType: String,
  val entityId: Int
): RuntimeException("$entityType with id $entityId not found")
class EntityNotFoundResponse(
  val entityType: String,
  val entityId: Int
):
ErrorResponse(
  code = "$entityType_not_found",
  message ="$entityType not found: ID=$entityId",
  solution = "Provide another ID or create a new $entityType"
)

And the same for EntityResourceNotFoundException


That’s all about error handling in RESTful web applications. Hope this helps!

Links

  1. Providing error response in RESTful API. Part 1
  2. Providing error response in RESTful API. Part 2
  3. Providing error response in RESTful API. Part 3

Published by Andrey Minogin

Full-stack Web/Java developer

Leave a comment

Your email address will not be published. Required fields are marked *