Table of contents:
A running instance of a ktor application is represented by Application class. A ktor application consists of a set of modules (possibly one). Each module is a regular kotlin lambda or a function (usually having an instance of application as a receiver or parameter).
An application is started inside of an environment that is represented by ApplicationEnvironment having an application config (See Configuration page for more details).
A ktor server is started with an environment and controls the application lifecycle. An application instance is created and destroyed by the environment (depending on the implementation it could create it lazily or provide hot reload functionality). So stopping the application doesn’t always mean that the server is stopping: for example, it could be reloaded while the server keeps running.
Application modules are started one by one when an application is started, and every module can configure an instance of the application. An application instance is configured by installing features and intercepting pipelines.
See lifecycle for more details.
A feature is a piece of specific functionality that could be plugged into an application. It usually intercepts
requests and responses and does its particular functionality.
For example, the Default Headers feature intercepts responses
and appends Date
and Server
headers. A feature can be installed into an application using the install
function
like this:
application.install(DefaultHeaders) {
// configure feature
}
For some features, the configuration lambda is optional. In this case, the feature can only be installed once. However,
there are cases when a configuration composition is required. For such features, there are helper functions
that install a feature if it is not yet installed and apply a configuration lambda. For example, routing {}
.
In ktor a pair of incoming request and response (complete or incomplete)
is named ApplicationCall.
Every application call is passed through an ApplicationCallPipeline
consisting of several (or none) interceptors. Interceptors are invoked one by one and every interceptor
can amend the request or response and control pipeline execution by proceeding (proceed()
) to the next interceptor
or finishing (finish()
or finishAll()
) the whole pipeline execution
(so the next interceptor is not invoked,
see PipelineContext for details).
It can also decorate the remaining interceptors chain doing additional actions before and after proceed()
invocation.
Consider the following decorating example:
intercept {
myPrepare()
try {
proceed()
} finally {
myComplete()
}
}
A pipeline may consist of several phases. Every interceptor is registered at a particular phase. So interceptors are executed in their phases order. See Pipelines documentation for a more detailed explanation.
An application call consists of a pair of request with response and a set of parameters.
So an application call pipeline has a pair of receive and send pipelines. The request’s content (body)
could be received using ApplicationCall.receive<T>()
where T
is an expected type of content.
For example, call.receive<String>()
reads the request body as a String
. Some types could be received with no
additional configuration
out of the box, while receiving a custom type may require a feature installation or configuration.
Every receive()
causes the receive pipeline (ApplicationCallPipeline.receivePipeline
)
to be executed from the beginning so every receive pipeline interceptor could transform or by-pass the request body.
The original body object type is ByteReadChannel
(asynchronous byte channel).
An application response body could be provided by ApplicationCall.respond(Any)
function invocation that
executes a response pipeline (ApplicationCallPipeline.respondPipeline
). Similar to receive pipeline,
every response pipeline interceptor could transform the response object. Finally, a response object should be
converted into an instance of
OutgoingContent.
A set of extension functions respondText
, respondBytes
, receiveText
, receiveParameters
and so on simplify the construction of request and response objects.
An empty application has no interceptors so 404 Not Found will be generated for every request. An application call pipeline should be intercepted to handle requests. An interceptor can respond depending on the request URI like this:
intercept {
val uri = call.request.uri
when {
uri == "/" -> call.respondText("Hello, World!")
uri.startsWith("/profile/") -> { TODO("...") }
}
}
For sure, this approach has a lot of disadvantages. Fortunately, there is the Routing feature for structured request handling that intercepts the application call pipeline and provides a way to register handlers for routes. Since the only thing Routing does is intercept the application call pipeline, manual interception with Routing also works. Routing consists of a tree of routes having handlers and interceptors. A set of extension functions in ktor provides an easy way to register handlers like this:
routing {
get("/") {
call.respondText("Hello, World!")
}
get("/profile/{id}") { TODO("...") }
}
Notice that routes are organized into a tree so you can declare structured routes:
routing {
route("profile/{id}") {
get("view") { TODO("...") }
get("settings") { TODO("...") }
}
}
A routing path can contain constant parts and parameters such as {id}
in the example above.
The property call.parameters
provides access to the captured setting values.
ContentNegotiation provides a way to negotiate mime types and convert types using Accept and Content-Type headers. A content converter can be registered for a particular content type for receiving and responding objects. There are Jackson, Gson and kotlinx.serialization content converters available out of the box that can be plugged into the feature.
Example:
install(ContentNegotiation) {
gson {
// Configure Gson here
}
}
routing {
get("/") {
call.respond(MyData("Hello, World!"))
}
}