Environment-Based Error Handling With Spring Boot and Kotlin
Tags: kotlin, programming, softwaredevelopment, softwareengineering
Published: 2024-01-19 20:58:18

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](https://writech.run/blog/what-a-recurring-outofmemory-error-taught-me-11f2061063a1), the default Spring Boot behavior is to return a [stack trace](https://en.wikibooks.org/wiki/Java_Programming/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](https://help.semmle.com/wiki/display/JAVA/Information+exposure+through+a+stack+trace)

The main goal of the approach we are going to present is to automatically adapt error messages according to the current [deployment environment](https://en.wikipedia.org/wiki/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](https://spring.io/projects/spring-boot) and [Kotlin](https://kotlinlang.org/).

## Handling Different Deployment Environments

To understand which deployment environment we are in, we will use a custom [environment variable](https://docs.oracle.com/javase/tutorial/essential/environment/env.html) 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:

```java
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:

```kotlin
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`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html) and [`@ControllerAdvice`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ControllerAdvice.html) 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](https://www.toptal.com/java/spring-boot-rest-api-error-handling)

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:

```kotlin
@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](https://www.baeldung.com/java-stacktrace-to-string#conversion-with-core-java)) and passes it to the `ErrorResponse` constructor only when not in the production environment.

Example of the same error response in the production environment:

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

And staging:

```json
{
  "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.
  
***
_The post "[Environment-Based Error Handling With Spring Boot and Kotlin](https://writech.run/blog/environment-based-error-handling-with-spring-boot-and-kotlin-b36b901135ad/)" appeared first on [Writech](https://writech.run)._