
Current state of affairs
Honestly most public APIs I’ve used have a bit of sanity in their error messaging. Usually the format for the error is uniform from resource to resource, which makes integration for front end applications a peaceful process. But the focus of this article is to address a trend in API development, using empty responses and status code based exceptions to handle “security”. I know you’re reading this, Mr. “returning a 500 keeps it secure by not sending any info” and I’m here to tell you….you’re right. But that chain of thought strangle holds error handling for the rest of the API. This methodology, with an origin that smells similar to ‘security by obscurity’, is often used instead of developing a flexible/evolvable strategy.
Sounds good but what do you know?
At the publishing date of this article, I work for a consultant company named Inrhythm. Before working there, I worked for Accenture. While at these consultant companies, I’ve exclusively worked in fintech and the “exception solution” mentioned before is a big favorite in these financial systems. A host of issues have manifested for such a short term gain strategy to dealing with errors. One issue, I won’t mention the client, had an affinity for returning 500 errors back when a request was found to be problematic. A simple 500, though annoying, generally can be handled by the front end. But let’s thinking about how it’s implemented. “If you do this one thing and get a 500….then you know this process failed.” The flaw in this logic speaks for itself, especially if a single business process calls multiple microservices to complete. You will have a consuming application writing custom error handling logic per process, rather than it having standard exception formats. Considering the commonality of this methodology, I know…I’m commiting a major act of blasphemy. But returning sensitive information is something you want to handle with control, rather than aborting ship and closing all forms of communication.
This isn’t new, but you’ve forgotten it
We’ve done this before, with something we all see. The login/sign in page. This simple mechanic for logging into web applications provides us a basic framework to handle errors. For example, the following json payload is in response to a failed login attempted.
Bad Example
Http Status: 401
Content-Type: Application/json
body: {
"messasge": "User password is incorrect"
}
This is a big no no. The API is giving too much information from this message alone. So let’s change it to what is more commonly seen nowadays:
Good Example
Http Status: 401
Content-Type: Application/json
body: {
"message": "User name/password is incorrect"
}
See, I’m not completely off my rocker. This error is commonly what we see today, it doesn’t give the consumer too much info, and a consumer can construct a simple error handling with this. But the days of the simple body has long passed, we need more information. So lets evolve the body to something like this:
{
"errorType": SOME_ERROR_HERE,
"time": 12:00:00PM,
"message": "A typical error message"
}
Typically I leave the “errorType” field up to a list of possible enums that I can easily share with the front end. I like the time value in general with my responses and the message field hasn’t changed. Now with this more fleshed out body, let’s handle something bigger than login, common in fintech, and requires the highest standard for exception handling; credit card saving.
The following json request is something typical for saving credit card credentials:
{
"cardNumber": "1234567812345678",
"expiration": 0323,
"name": "Human Caching",
"cvc": 298
}
Bad Response Error
Http Status: 401
Content-Type: Application/json
body: {
"errorType": CARD_SAVING_ERROR,
"time": 12:00:00PM,
"message": "You card number is wrong, fix that and you're fine. No way this will be exploited and bite us in the butt at all"
}
Better Response Error
Http Status: 401
Content-Type: Application/json
body: {
"errorType": CARD_SAVING_ERROR,
"time": 12:00:00PM
"message": ""
}
Now reading the message from the bad response, pretty much explains why it isn’t good. But the most important thing here sending the type of error, doesn’t hurt us in either case and consumers have a point of reference in which to construct robust/meaningful errors for users.
Code for the road
I’ve been a big fan of writing Spring Boot in Kotlin for new backend services I work on in my spare time. I used some the language features, like default arguments and optional arguments to implement my exception responses. First let’s go over the open class driving the exceptions we will define later:
open class ApiException(val status: HttpStatus, val errorType: ErrorTypes, message: String?): Exception(message)
Now if you’re unfamiliar with Kotlin, the syntax may look a bit funky. So let’s break it down in parts:
Exception(message) - The class we define is inheriting it's the constructor of the Exception class. It takes a String parameter in it's constructor message: String? - This is the nullable field in our ApiException class. When defining a classes that inherit it, it is optional to provide a value. However creating an instance of the class requires it's value. open - Allows overriding of properties in classes that inherit it. Therefore we can override all values in ApiException's constructor and classes that inherit from it.
For the Spring portion, we simply implement a ControllerAdvice. It basically works as a proxy for when exceptions, specificed by the methods in it’s class (using @ExceptionHandler), are thrown. So we will have it interrupt our ApiException class:
@ControllerAdvice
class ApiExceptionController : ResponseEntityExceptionHandler() {
@ExceptionHandler(value = [(ApiException::class)])
fun handleApiException(ex: ApiException, request: WebRequest): ResponseEntity<ErrorDetails> {
val errorDetails = ErrorDetails(details = ex.errorType, message = ex.message)
return ResponseEntity.status(ex.status).body(errorDetails)
}
}
Since these two parts are implemented, we can now define custom exceptions that can be thrown and automatically handled by our ControllerAdvice.
Sample Exception Definition
class FeedNotFoundException(status: HttpStatus = HttpStatus.NOT_FOUND, errorType: ErrorTypes = ErrorTypes.FEED_NOT_FOUND, message: String?): ApiException(status, errorType, message)
How to throw it
val feed = feedRepository.findFeedByGroupId(groupId).orElseThrow{FeedNotFoundException(message = "Feed does not exist for group") }
Notice during the exception definition I passed in a message. But due to the power of Kotlin, I could of not passed a message or I could of changed the default HttpStatus, or ErrorType.
Customizing your throw
val feed = feedRepository.findFeedByGroupId(groupId).orElseThrow{FeedNotFoundException(status = HttpStatus.BAD_REQUEST, errorTypes = ErrorTypes.USER_NOT_ALLOWED, message = "") }
With this level of customization, we can throw errors with highly customizable fields, with a single standardized format, and can be morphed to fit our business logic needs. In an update to this article, I will provide the code in gist, so it’s easier to follow overall.
Moving into sanity
Besides showing off how cool Kotlin is, I hope my case for descriptive errors has made an impression on you. The common habits for securing API’s from common security holes, like enumeration attacks, should/can change utilizing modern tools we have at our disposal. A fluid, evolvable error throwing strategy beats a simple response code based one. Helping users/creators who absorb our services, while not sacrificing security, is my definition of a friendly API.
Resources
I’m hooked on Kotlin and anyone I know who tries it, tends to follow along too. If you want more information on the Kotlin and utilizing Spring with it, please check the resources below:
