Handling HTTP Requests

预计阅读时间: 12 分钟

When handling routes, or directly intercepting the pipeline, you get a context with an ApplicationCall. That call contains a property called request that includes information about the request.

Also, the call itself has some useful convenience properties and methods that rely on the request.

Table of contents:

Introduction

When using the Routing feature, or when intercepting requests, you can access the call property inside handlers. That call includes a request property with relevant information about the request:

routing {
    get("/") {
        val uri = call.request.uri
        call.respondText("Request uri: $uri")
    } 
}

intercept(ApplicationCallPipeline.Call) { 
    if (call.request.uri == "/") {
        call.respondText("Test String")
    }
}

Request information

As part of the request, you can get access to its internal context:

val call: ApplicationCall = request.call
val pipeline: ApplicationReceivePipeline = request.pipeline

URL, method, scheme, protocol, host, path, httpVersion, remoteHost, clientIp

val version: String = request.httpVersion // "HTTP/1.1"
val httpMethod: HttpMethod = request.httpMethod // GET, POST... 
val uri: String = request.uri // Short cut for `origin.uri`
val scheme: String = request.origin.scheme // "http" or "https"
val host: String? = request.host() // The host part without the port 
val port: Int = request.port() // Port of request
val path: String = request.path() // The uri without the query string
val document: String = request.document() // The last component after '/' of the uri
val remoteHost: String = request.origin.remoteHost // The IP address of the client doing the request

Reverse proxy support: origin and local

When behind a reverse-proxy (for example an nginx or a load balancer), the received request is not performed by the end-user, but that reverse proxy. That means that the client IP address of the connection would be the one of the proxy instead of the client. Also the reverse proxy might be serving via HTTPS and requesting to your server via HTTP. Popular reverse proxies send X-Forwarded- headers to be able to access this information.

Note that for this to work when under a reverse-proxy you have to install the XForwardedHeaderSupport feature.

As part of the request object, there are two properties local and origin that allows to get information of the original request or the local/proxied one.

val local : RequestConnectionPoint = request.local // Local information 
val origin: RequestConnectionPoint = request.origin // Local / Origin if XForwardedHeaderSupport feature is installed.

The local/origin information you can get:

interface RequestConnectionPoint {
    val scheme: String // "http" or "https": The provided protocol (local) or `X-Forwarded-Proto`
    val version: String // "HTTP/1.1"
    val port: Int
    val host: String // The provided host (local) or `X-Forwarded-Host`
    val uri: String
    val method: HttpMethod
    val remoteHost: String // The client IP (the direct ip for `local`, or the redirected one `X-Forwarded-For`)
}

GET / Query parameters

If you need to access the query parameters ?param1=value&param2=value as a collection, you can use queryParameters. It implements the StringValues interface where each key can have a list of Strings associated with it.

val queryParameters: Parameters = request.queryParameters
val param1: String? = request.queryParameters["param1"] // To access a single parameter (first one if repeated)
val repeatedParam: List<String>? = request.queryParameters.getAll("repeatedParam") // Multiple values

You can also access the raw queryString (param1=value&param2=value):

val queryString: String = request.queryString()

POST, PUT and PATCH

POST, PUT and PATCH requests has an associated request body (the payload). That payload is usually encoded.

All the receive methods consume the whole payload sent by the client so an attempt to receive a request body twice will lead to RequestAlreadyConsumedException error unless you have DoubleReceive feature installed.

Raw payload

To access the raw bits of the payload, you can use receiveChannel, but it is directly part of the call instead of call.request:

val channel: ByteReadChannel = call.receiveChannel()

And it provide some convenience methods for common types:

val channel: ByteReadChannel = call.receiveChannel()
val text: String = call.receiveText()
val inputStream: InputStream = call.receiveStream() // NOTE: InputStream is synchronous and blocks the thread
val multipart: MultiPartData = call.receiveMultipart()

All those receive* methods are aliases of call.receive<T> with the specified type. The types ByteReadChannel, ByteArray, InputStream, MultiPartData, String and Parameters are handled by ApplicationReceivePipeline.installDefaultTransformations that is installed by default.

Form Parameters (urlencoded or multipart)

To parse a form urlencoded or with multipart, you can use receiveParameters or receive<Parameters>:

val postParameters: Parameters = call.receiveParameters()

Receive Typed Objects, Content-Type and JSON

The call also supports receiving generic objects:

val obj: T = call.receive<T>()
val obj: T? = call.receiveOrNull<T>()

In order to receive custom objects from the payload, you have to use the ContentNegotiation feature. This is useful for example to receive and send JSON payloads in REST APIs.

install(ContentNegotiation) {
    gson {
        setDateFormat(DateFormat.LONG)
        setPrettyPrinting()
    }
}

If you configure the ContentNegotiation to use gson, you will need to include the ktor-gson artifact:

compile("io.ktor:ktor-gson:$ktor_version")

Then you can, as an example, do:

data class HelloWorld(val hello: String)

routing {
    post("/route") {
        val helloWorld = call.receive<HelloWorld>()
    }
}

Remember that your classes must be defined top level (outside of any other class or function) to be recognized by Gson.

Multipart, Files and Uploads

Check the uploads section.

Custom receive transformers

You can create custom transformers by calling application.receivePipeline.intercept(ApplicationReceivePipeline.Transform) { query -> and then calling proceedWith(ApplicationReceiveRequest(query.type, transformed)) as does the ContentNegotiation feature.

Cookies

There is a cookies property to access the Cookie headers sent by the client, just as if it was a collection:

val cookies: RequestCookies = request.cookies
val mycookie: String? = request.cookies["mycookie"]

To handle sessions using cookies, have a look to the Sessions feature.

Headers

To access the headers the request objects has a headers: Headers property. It implements the StringValues interface where each key can have a list of Strings associated with it.

val headers: Headers = request.headers
val header: String? = request.header("HeaderName") // To access a single header (first one if repeated)
val repeatedHeader: List<String>? = request.headers.getAll("HeaderName") // Multiple values

And several convenience methods to access some common headers:

val contentType: ContentType = request.contentType() // Parsed Content-Tpe 
val contentCharset: Charset? = request.contentCharset() // Content-Type JVM charset
val authorization: String? = request.authorization() // Authorization header
val location: String? = request.location() // Location header
val accept: String? = request.accept() // Accept header
val acceptItems: List<HeaderValue> = request.acceptItems() // Parsed items of Accept header
val acceptEncoding: String? = request.acceptEncoding() // Accept-Encoding header
val acceptEncodingItems: List<HeaderValue> = request.acceptEncodingItems() // Parsed Accept-Encoding items 
val acceptLanguage: String? = request.acceptLanguage() // Accept-Language header
val acceptLanguageItems: List<HeaderValue> = request.acceptLanguageItems() // Parsed Accept-Language items
val acceptCharset: String? = request.acceptCharset() // Accept-Charset header
val acceptCharsetItems: List<HeaderValue> = request.acceptCharsetItems() // Parsed Accept-Charset items
val userAgent: String? = request.userAgent() // User-Agent header
val cacheControl: String? = request.cacheControl() // Cache-Control header
val ranges: RangesSpecifier? = request.ranges() // Parsed Ranges header

val isChunked: Boolean = request.isChunked() // Transfer-Encoding: chunked
val isMultipart: Boolean = request.isMultipart() // Content-Type matches Multipart