Computer scienceBackendKtorKtor Plugins

Request Validation

1 minute read

As developers, it's crucial for us to ensure that the incoming requests to our applications are valid. Invalid or malformed requests can lead to a plethora of issues, ranging from corrupted data to security vulnerabilities. This is where the RequestValidation plugin comes into play.

In this guide, we will dive deeper into how to use RequestValidation to safeguard your Ktor applications. Our exploration will involve adding necessary dependencies, installing the plugin, configuring it, and handling any exceptions that arise. Furthermore, we will validate object properties and byte arrays with illustrative examples.

Adding Dependencies

The first step towards using the RequestValidation plugin is to add the necessary dependencies. You need to include the ktor-server-request-validation artifact in your build script. Here is how you can do it:

implementation("io.ktor:ktor-server-request-validation:$ktor_version")

Installing RequestValidation

After adding the necessary dependencies, you can install the RequestValidation plugin in your Ktor application. This installation can take place either inside the embeddedServer function call or inside an explicitly defined module that is an extension function of the Application class.

Let's say you have a simple embeddedServer function call. You can install the RequestValidation plugin by adding the install(RequestValidation) line to this server setup:

import io.ktor.server.application.*
import io.ktor.server.plugins.requestvalidation.*

fun main() {
    embeddedServer(Netty, port = 8080) {
        install(RequestValidation)
        // ...
    }.start(wait = true)
}

Similarly, if you have an explicitly defined module function, you can install the plugin as follows:

fun Application.module() {
    install(RequestValidation)
    // rest of your application setup here
}

Configuration of RequestValidation

The configuration of RequestValidation involves three main steps:

  1. Receiving body contents: The RequestValidation plugin validates the body of a request if you call the receive function with a type parameter. The following example shows how to receive a body as a Double value:
    routing {
        post("/number") {
            val body = call.receive<Double>()
            call.respond(body)
        }
    }
  2. Configuring a validation function: Use the validate function to validate a request body. Suppose we want to validate that a received Double value is positive:
    install(RequestValidation) {
        validate<Double> { number ->
            if (number <= 0) ValidationResult.Invalid("Number should be positive")
            else ValidationResult.Valid
        }
    }
  3. Handling validation exceptions: If a RequestValidationException is raised, the server can respond with a suitable error message. For example:
    install(StatusPages) {
        exception<RequestValidationException> { call, cause ->
            call.respond(HttpStatusCode.BadRequest, cause.reasons.joinToString())
        }
    }

Validating Object Properties

Suppose you have a Product data class with id and name properties:

@Serializable
data class Product(val id: Int, val name: String)

You can install the ContentNegotiation plugin with the JSON serializer:

install(ContentNegotiation) {
    json()
}

Configure validation:

install(RequestValidation) {
    validate<Product> { product ->
        if (product.id <= 0) ValidationResult.Invalid("A product ID should be greater than 0")
        else ValidationResult.Valid
    }
}

Then receive a Product object:

post("/product") {
    val product = call.receive<Product>()
    call.respond(product)
}

Validating Byte Arrays

Suppose you receive data as a byte array. In this code snippet, the server is set to handle a POST request at the "/byte-array" endpoint. It receives the body of the request as a byte array and then responds with the same body:

post("/byte-array") {
    val body = call.receive<ByteArray>()
    call.respond(String(body))
}

For byte arrays, we'll use an overload of the validate function that accepts a ValidatorBuilder. This allows us to set custom validation rules.

install(RequestValidation) {
    validate {
        filter { body -> body is ByteArray }
        validation { body ->
            check(body is ByteArray)
            val intValue = String(body).toInt()
            if (intValue <= 0)
                ValidationResult.Invalid("A value should be greater than 0")
            else ValidationResult.Valid
        }
    }
}

The filter function is used to ensure that the validation is only applied to byte arrays. The validation function then checks if the body is indeed a byte array. It converts the byte array to an integer and checks if the integer value is greater than 0. If the value is less than or equal to 0, it returns ValidationResult.Invalid with a custom error message. If the value is greater than 0, it returns ValidationResult.Valid to indicate that the validation was successful.

Make sure that the byte array can be correctly converted to the desired type before validation, as this might not always be the case. Use try-catch blocks to handle potential conversion errors.

Conclusion

In this guide, we've explored how to use Ktor's RequestValidation plugin to validate incoming request data in your Ktor application. We've looked at how to validate strings, byte arrays, and object properties. This plugin provides a flexible way to ensure the data your application receives is always valid and adheres to your criteria.

Always remember to handle RequestValidationException properly to provide useful feedback to the client in case of validation failures. By doing so, you can improve the resilience of your application and ensure a better experience for your users. With Ktor's RequestValidation plugin, creating robust and secure Ktor applications is easier than ever.

How did you like the theory?
Report a typo