Skip to content

Environment-Based Error Handling With Spring Boot and Kotlin

Simplifying debugging in the staging environment

Providing simple but meaningful messages while handling errors in web services should be the preferred approach. This way, we will not expose any internal information about the causes of the errors and we will help the API client properly respond to issues.

When an error occurs, the default Spring Boot behavior is to return a stack trace. The main downside of this approach is that it might leak useful information about our implementation to a potential attacker. On the other hand, stack traces — unlike meaningful messages — are extremely important for debugging.

“Stack traces can tell the developer more about the sequence of events that led to a failure, as opposed to merely the final state of the software when the error occurred. Unfortunately, the same information can be useful to an attacker. The sequence of class names in a stack trace can reveal the structure of the application as well as any internal components it relies on.” — Semmle

The main goal of the approach we are going to present is to automatically adapt error messages according to the current deployment environment. In the staging environment, a simple and meaningful message, as well as a stack trace, will be returned. In the production environment, we will expose only the first one.

Let’s see how this can be achieved in Spring Boot and Kotlin.

Handling Different Deployment Environments

To understand which deployment environment we are in, we will use a custom environment variable called ENV. We assume that its value is PRODUCTION in the production environment and STAGING in the staging environment. In Kotlin, environment variables value can be retrieved as follows:

System.getenv("env_name")

Defining a Custom Error Class

We are going to define a custom class called ErrorMessage to represent API errors. The goal of this class is to wrap exceptions in a nice JSON representation to make life easier for API clients.

We can implement such a class as follows:

class ErrorResponse(
        status: HttpStatus,
        val message: String,
        var stackTrace: String? = null
) {

    val code: Int
    val status: String

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "MM-dd-yyyy hh:mm:ss")
    val timestamp: Date

    init {
        timestamp = Date()
        code = status.value()
        status = status.name()
    }
}

As you can notice, the stackTrace attribute is nullable and optional since it will be used in the staging environment only.

Spring Boot Environment-Based Error Handling

Using @ExceptionHandler and @ControllerAdvice is one of the main methods to handle errors since Spring Boot 3.2.

@ExceptionHandler is a Spring annotation that provides a mechanism to treat exceptions that are thrown during execution of handlers (Controller operations). This annotation, if used on methods of controller classes, will serve as the entry point for handling exceptions thrown within this controller only. Altogether, the most common way is to use @ExceptionHandler on methods of @ControllerAdvice classes so that the exception handling will be applied globally or to a subset of controllers.” — Toptal

Thanks to them, we can build a global error handling component called ControllerExceptionHandler. Its goal is to catch exceptions and wrap them in ErrorMessage objects, which will be serialized into JSON and sent back to API clients. It also implements the environment-based logic, as shown below:

@ControllerAdvice
class ControllerExceptionsHandler {

    @ExceptionHandler(
            ConstraintViolationException::class,
            HttpClientErrorException.BadRequest::class,
            MethodArgumentNotValidException::class,
            MissingServletRequestParameterException::class,
            IllegalArgumentException::class
    )
    fun constraintViolationException(e: Exception): ResponseEntity<ErrorResponse> {
        return generateErrorResponse(HttpStatus.BAD_REQUEST, "Bad request", e)
    }

    @ExceptionHandler(AuthorizationException::class)
    fun unauthorizedException(e: Exception): ResponseEntity<ErrorResponse> {
        return generateErrorResponse(HttpStatus.FORBIDDEN, "You are not authorized to do this operation", e)
    }

    @ExceptionHandler(AuthenticationException::class)
    fun forbiddenException(e: Exception): ResponseEntity<ErrorResponse> {
        return generateErrorResponse(HttpStatus.UNAUTHORIZED, "You are not allowed to do this operation", e)
    }

    @ExceptionHandler(
            EntityNotFoundException::class,
            NoSuchElementException::class,
            NoResultException::class,
            EmptyResultDataAccessException::class,
            IndexOutOfBoundsException::class,
            KotlinNullPointerException::class
    )
    fun notFoundException(e: Exception): ResponseEntity<ErrorResponse> {
        return generateErrorResponse(HttpStatus.NOT_FOUND, "Resource not found", e)
    }

    @ExceptionHandler(
            Exception::class
    )
    fun internalServerErrorException(e: Exception): ResponseEntity<ErrorResponse> {
        return generateErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Generic internal error", e)
    }

    private fun generateErrorResponse(
            status: HttpStatus, 
            message: String, 
            e: Exception
    ): ResponseEntity<ErrorResponse> {
        // converting the exception stack trace to a string
        val sw = StringWriter()
        val pw = PrintWriter(sw)
        e.printStackTrace(pw)
        val stackTrace = sw.toString()

        // example: logging the stack trace
        // log.debug(stackTrace)

        // environment-based logic
        val stackTraceMessage =
                when (System.getenv("ENV").toUpperCase()) {
                    "STAGING" -> stackTrace // returning the stack trace
                    "PRODUCTION" -> null // returning no stack trace
                    else -> stackTrace // default behavior
                }

        return ResponseEntity(ErrorResponse(status, message, stackTraceMessage), status)
    }

}

The most important method is generateResponse, which converts the exception stack trace to a String (as described in this course) and passes it to the ErrorResponse constructor only when not in the production environment.

Example of the same error response in the production environment:

{
  "timestamp": "09-21-2020 15:51:17",
  "code": 404,
  "status": "NOT_FOUND",
  "message": "Resource not found"
}

And staging:

{
  "timestamp": "09-21-2020 15:51:17",
  "code": 404,
  "status": "NOT_FOUND",
  "message": "Resource not found",
  "stackTrace": "Exception in thread \"main\" kotlin.KotlinNullPointerException at com.test.ApplicationKt.throwNullPointerException(Application.kt:8) at com.test.ApplicationKt.throwNullPointerException$default(Application.kt:7) at com.test.ApplicationKt.main(Application.kt:4)"
}

This way, the message attribute can be used to explain what happened to users. In the staging environment, developers can understand why the error occurred thanks to the stackTrace attribute.

Conclusion

Not being able to understand why an error occurred can be frustrating! Exposing detailed error descriptions can be dangerous. There is a way to cautiously face this issue, however. Using the approach above, when an error occurs, users will always see a simple message. But when in the staging environment, developers will have everything they need to debug it.

That’s all, folks! I hope this helps you handle errors in Spring Boot and Kotlin.

nv-author-image

Antonello Zanini

I'm a software engineer, but I prefer to call myself a Technology Bishop. Spreading knowledge through writing is my mission.View Author posts

Want technical content like this in your blog?