You can't control absolutely everything. The user may enter incorrect data, and the resource from which you receive some data may be unavailable. Such situations are called exceptions – when unexpected errors occur during the execution of the program.
Introduction
To begin with, let's create a small application that takes two numbers a and b and divides them. As you can see, many possible errors can occur:
The user did not pass a required parameter.
The user entered the data incorrectly: no numbers, just one number, invalid format, etc.
The number
bis zero.
Taking these errors into account, it is useful to specify custom exception classes:
class ParameterNotPassedException(name: String) : Exception("The '$name' parameter is not passed")
class InvalidParameterException(name: String) : Exception("The '$name' parameter is invalid")
class DivisionByZeroException : Exception("Division by zero")By applying the appropriate checks, we create the following application:
fun Application.module() {
configureRouting()
}
fun Application.configureRouting() {
routing {
get("calc") {
// receive query parameters from client
val parameters = call.request.queryParameters
// check whether the user has passed the necessary parameters
if ("a" !in parameters) throw ParameterNotPassedException("a")
if ("b" !in parameters) throw ParameterNotPassedException("b")
// check whether these parameters can be converted to integers
val a = parameters["a"]!!.toIntOrNull() ?: throw InvalidParameterException("a")
val b = parameters["b"]!!.toIntOrNull() ?: throw InvalidParameterException("b")
// check whether b is nonzero
if (b == 0) throw DivisionByZeroException()
// send the result to the client
call.respondText("$a / $b = ${a / b}")
}
}
}To perform calculations, the client has to pass query parameters via the path /calc, for example:
http://localhost:8080/calc?a=15&b=3First, we check whether the user has passed the necessary parameters. If a parameter is missing, we throw the custom ParameterNotPassedException exception.
Next, we check whether these parameters can be converted to integers. Finally, we check whether b is nonzero.
An exception may occur at any stage of these checks. If one does occur, the user will not be able to access the service, as it will fail with an error. The user will receive a 500 Internal Server Error response, and the unhandled error will be visible in the server logs:
This is inconvenient because the user will not understand what went wrong. To respond appropriately to any exception, we can use the StatusPages plugin.
Installation
To use StatusPages, you need to include the ktor-server-status-pages artifact in your build script:
implementation("io.ktor:ktor-server-status-pages:$ktor_version")implementation "io.ktor:ktor-server-status-pages:$ktor_version"Then, you can pass the plugin to the install function in your primary module (or any other appropriate module). Note that you will need to import the plugin from io.ktor.server.plugins.statuspages.*:
fun Application.module() {
install(StatusPages)
// other configuration
}Exception handling
The StatusPages plugin can be configured to handle three types of events:
exceptions — the response is based on the mapped exception class.
status — the response is based on the mapped status code value.
status file — configures a file response from the classpath.
The exception handler manages calls that result in a Throwable exception. For example, the 500 HTTP status code can be configured for any exception:
install(StatusPages) {
exception<Throwable> { call, cause ->
call.respondText(text = "500: $cause" , status = HttpStatusCode.InternalServerError)
}
}Notice that the server logs no longer show an unhandled error, since the exception is captured and consistently returned as a 500 response.
If you have a custom exception class, you can create a handler specifically for it. For example, if you want to handle ParameterNotPassedException and InvalidParameterException differently:
install(StatusPages) {
exception<InvalidParameterException> { call, _ ->
call.respondText(text = "400: Parameter is invalid", status = HttpStatusCode.BadRequest)
}
exception<ParameterNotPassedException> { call, _ ->
call.respondText(text = "400: Parameter is not passed", status = HttpStatusCode.BadRequest)
}
}You can also check for specific exceptions within a general handler using standard conditional checks. This is useful is you want to group logic under a generic Throwable handler but still make distinctions:
install(StatusPages) {
exception<Throwable> { call, cause ->
when (cause) {
is InvalidParameterException -> {
call.respondText(text = "400: Parameter is invalid", status = HttpStatusCode.BadRequest)
}
is ParameterNotPassedException -> {
call.respondText(text = "400: Parameter is not passed", status = HttpStatusCode.BadRequest)
}
is DivisionByZeroException -> {
call.respondText(text = "400: Division by zero", status = HttpStatusCode.BadRequest)
}
}
}
}Status handling
The status handler provides the capability to respond with specific content based on the status code. The example below shows how to respond to requests when a resource is missing on the server (the 404 HTTP status code):
install(StatusPages) {
status(HttpStatusCode.NotFound) { call, status ->
// 'status' holds the intercepted status code
call.respondText(text = "404: There is no such page :(", status = status)
}
}Status files
The statusFile handler allows you to serve HTML pages based on the status code. Consider a project with the following structure:
ktor-status-pages/
└── src/
└── main/
├── kotlin/
│ └── org.hyperskill.ktor/
│ └── Application.kt
└── resources/
└── pages/
└── errors/
├── 400.html
├── 404.html
└── 500.htmlNow, you can handle the 400, 404, 500 status codes using statusFile as follows:
install(StatusPages) {
statusFile(
HttpStatusCode.BadRequest,
HttpStatusCode.NotFound,
HttpStatusCode.InternalServerError,
filePattern = "pages/errors/#.html"
)
}The statusFile handler replaces any # character with the value of the status code within the list of configured statuses.
Note that the server will search for the file relative to the resources folder.
Example
As you remember, we wrote an application that can divide two numbers. We can handle three types of exceptions:
ParameterNotPassedExceptionInvalidParameterExceptionDivisionByZeroException
We install and configure the StatusPages plugin to handle these exceptions:
fun Application.configureStatusPages() {
install(StatusPages) {
exception<ParameterNotPassedException> { call, _ ->
call.respondText(
text = "400: Parameter is not passed\n\n$cause", status = HttpStatusCode.BadRequest
)
}
exception<InvalidParameterException> { call, _ ->
call.respondText(
text = "400: Parameter is invalid\n\n$cause", status = HttpStatusCode.BadRequest
)
}
exception<DivisionByZeroException> { call, _ ->
call.respondText(
text = "400: Division by zero", status = HttpStatusCode.BadRequest
)
}
statusFile(
HttpStatusCode.NotFound,
filePattern = "pages/errors/#.html"
)
}
}We also have to handle situations when a client requests a route that does not exist. When this happens, the server will return the contents of the file resources/pages/errors/404.html:
<pre>
_ _ ___ _ _ _ _ _ ______ _
| || | / _ \| || | | \ | | | | | ____| | |
| || |_| | | | || |_ | \| | ___ | |_ | |__ ___ _ _ _ __ __| |
|__ _| | | |__ _| | . ` |/ _ \| __| | __/ _ \| | | | '_ \ / _` |
| | | |_| | | | | |\ | (_) | |_ | | | (_) | |_| | | | | (_| |
|_| \___/ |_| |_| \_|\___/ \__| |_| \___/ \__,_|_| |_|\__,_|
</pre>Now, the application responds appropriately to any of these errors:
Conclusion
Exceptions, together with the StatusPages plugin, are a good way to deal with unexpected situations that may occur during execution. With this plugin, you can intercept exceptions and status codes and serve HTML pages based on the status code. So, don't let your server crash without any messages, as users need to know what happened.