HTTP Handlers
Module http_handlers
This port's purpose is to develop HTTP servers (REST services or Web applications). It defines a DSL
to declare HTTP request handlers.
Adapters implementing this port are in charge of processing HTTP requests through a list of
handlers.
Install the Dependency
This module is not meant to be used directly. It is used by HTTP client and HTTP server modules.
HTTP Routing Documentation (TODO)
- Filters/callbacks definitions and how they can be combined
- Design makes easy to test routing, not only callbacks
- Functional approach design (immutability, no side effects)
HTTP Context
These are the events that the handlers handle (they are also called HTTP calls or just calls along
this documentation). They wrap HTTP requests and responses along with some attributes that may be
used to pass data across handlers or to signal that a previous callback resulted in an error.
The HTTP context provides you with everything you need to handle an HTTP request. It contains the
request, the response, and a bunch of utility methods to return results, read parameters or pass
attributes among handlers.
The methods are available directly from the callback. You can check the API documentation for the
full list of methods. This sample code illustrates the usage:
1
2
3
4
5
6
7
8
9
10
11
12 | get("/call") {
attributes // The attributes list
attributes["foo"] // Value of foo attribute
ok("Response body") // Returns a 200 status
// return any status (previous return value is ignored here)
send(
BAD_REQUEST_400,
"Invalid request",
attributes = attributes + ("A" to "V") // Sets value of attribute A to V
)
}
|
Handlers
The main building blocks of Hexagon HTTP services are a set of handlers. A handler is made up of two
simple pieces:
- A predicate: which decides if the handler should be executed for a given request.
- A callback: code that receives an HTTP context and returns another (or the same) context.
IMPORTANT: the order in which handlers are declared is NOT the order, it is the depth. Handlers are
not linked, they are NESTED. The next() method passes control to the next level. If
next() is not called in a Handler, following Handlers WON'T be executed (However, the
"after" part of already executed Handlers will be run).
For example, this definition:
Is really this execution:
| H1 (on)
H2 (on)
H3 (on)
H2 (after)
H1 (after)
|
Check the next snippet for Handlers usage examples:
| get("/hello") { ok("Get greeting") }
put("/hello") { ok("Put greeting") }
post("/hello") { ok("Post greeting") }
on(ALL - GET - PUT - POST, "/hello") { ok("Fallback if HTTP verb was not used before") }
on(status = NOT_FOUND_404) { ok("Get at '/' if no route matched before") }
|
Handler Predicates
A predicate is a function that is applied to a call context and returns a boolean. If the result is
true, the handler will be executed.
The default implementation (HttpPredicate) is based on a template with any combination of
the following fields:
- a list of HTTP methods
- a path pattern
- an exception
- a status
It yields true if all the supplied fields matches a call context.
Path Patterns
Patterns to match requests paths. They can have:
- Variables:
/path/{param}
- Wildcards:
/*/path
- Regular expresión subset:
/(this|that)/path
Handler Types
On Handlers
- Predicate evaluated at start
- Executed at the start (before the 'next' handler is called)
After Handlers
- Predicate evaluated at end (checked on the coming back of the execution next handler)
- Executed at the end (after 'next' handler has returned)
Filters
You might know filters as interceptors, or middleware from other libraries. Filters are blocks of
code executed before and/or after other handlers. They can read the request, read/modify the
response, and call the remaining handlers or skip them to halt the call processing.
All filters that match a route are executed in the order they are declared.
Filters optionally take a pattern, causing them to be executed only if the request path matches
that pattern.
The following code details filters usage:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | before("/*") { send(response + Header("b-all", "true")) }
before("/filters/*") { send(response + Header("b-filters", "true")) }
get("/filters/route") { ok("filters route") }
after("/filters/*") { send(response + Header("a-filters", "true")) }
get("/filters") { ok("filters") }
path("/nested") {
before("*") { send(response + Header("b-nested", "true")) }
before { send(response + Header("b-nested-2", "true")) }
get("/filters") { ok("nested filters") }
get("/halted") { send(499, "halted") }
get { ok("nested also") }
after("*") { send(response + Header("a-nested", "true")) }
}
after("/*") { send(response + Header("a-all", "true")) }
|
Path Handlers
Handlers can be grouped by calling the path()
method, which takes a String prefix and gives you a
scope to declare other handlers. Ie:
| path("/nested") {
get("/hello") { ok("Greeting") }
path("/secondLevel") {
get("/hello") { ok("Second level greeting") }
}
get { ok("Get at '/nested'") }
}
|
If you have a lot of routes, it can be helpful to group them into Path Handlers. You can create path
handlers to mount a group of routes in different paths (allowing you to reuse them). Check this
snippet:
| fun personRouter(kind: String) = path {
get { ok("Get $kind") }
put { ok("Put $kind") }
post { ok("Post $kind") }
}
val server = HttpServer(serverAdapter(), HttpServerSettings(bindPort = 0)) {
path("/clients", personRouter("client"))
path("/customers", personRouter("customer"))
}
|
Handler Callbacks
Callbacks are request's handling blocks that are bound to handlers. They make the request and
response objects available to the handling code.
Callbacks produce a result by returning the received HTTP context with a different response.
Callbacks results are the input for the next handler's callbacks in the pipeline.
Request
Request functionality is provided by the request
field:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46 | get("/request") {
// URI Information
request.method // The HTTP method (GET, etc.)
request.protocol // HTTP or HTTPS
request.host // The host, e.g. "example.com"
request.port // The server port
request.path // The request path, e.g. /result.jsp
request.body // Request body sent by the client
method // Shortcut of `request.method`
protocol // Shortcut of `request.protocol`
host // Shortcut of `request.host`
port // Shortcut of `request.port`
path // Shortcut of `request.path`
// Headers
request.headers // The HTTP headers map
request.headers["BAR"]?.value // First value of BAR header
request.headers.getAll("BAR") // List of values of BAR header
// Common headers shortcuts
request.contentType // Content type of request.body
request.accept // Client accepted content types
request.authorization // Client authorization
request.userAgent() // User agent (browser requests)
request.origin() // Origin (browser requests)
request.referer() // Referer header (page that makes the request)
accept // Shortcut of `request.accept`
authorization // Shortcut of `request.authorization`
// Parameters
pathParameters // Map with all path parameters
request.formParameters // Map with all form fields
request.queryParameters // Map with all query parameters
request.parts // List of all body parts
queryParameters // Shortcut of `request.queryParameters`
formParameters // Shortcut of `request.formParameters`
parts // Shortcut of `request.parts`
// Body processing
request.contentLength // Length of request body
ok()
}
|
Path Parameters
Route patterns can include named parameters, accessible via the pathParameters
map on the request
object:
Path parameters can be accessed by name or by index.
| get("/pathParam/{foo}") {
pathParameters["foo"] // Value of foo path parameter
pathParameters // Map with all parameters
ok()
}
|
Query Parameters
It is possible to access the whole query string or only a specific query parameter using the
parameters
map on the request
object:
| get("/queryParam") {
request.queryParameters // The query params map
request.queryParameters["FOO"]?.value // Value of FOO query param
request.queryParameters.getAll("FOO") // All values of FOO query param
request.queryParameters.values // The query params list
ok()
}
|
HTML Form processing. Don't parse body!
| get("/formParam") {
request.formParameters // The form params map
request.formParameters["FOO"]?.value // Value of FOO form param
request.formParameters.getAll("FOO") // All values of FOO form param
request.formParameters.values // The form params list
ok()
}
|
File Uploads
Multipart Requests
| post("/file") {
val filePart = request.partsMap()["file"] ?: error("File not available")
ok(filePart.body)
}
|
Response
Response information is provided by the response
field:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | get("/response") {
response.body // Get response content
response.status // Get the response status
response.contentType // Get the content type
status // Shortcut of `response.status`
send(
status = UNAUTHORIZED_401, // Set status code to 401
body = "Hello", // Sets content to Hello
contentType = ContentType(APPLICATION_XML), // Set application/xml content type
headers = response.headers
+ Header("foo", "bar") // Sets header FOO with single value bar
+ Header("baz", "1")
+ Header("baz", "2") // Sets header FOO values with [ bar ]
)
// Utility methods for generating common responses
unauthorized("401: authorization missing")
forbidden("403: access not granted")
internalServerError("500: server error")
serverError(NOT_IMPLEMENTED_501, RuntimeException("Error"))
ok("Correct")
badRequest("400: incorrect request")
notFound("404: Missing resource")
created("201: Created")
redirect(FOUND_302, "/location")
found("/location")
// The response can be modified chaining send calls (or its utility methods)
ok("Replacing headers").send(headers = Headers())
ok("Intending to replace headers")
}
|
To send error responses:
| get("/halt") {
send(UNAUTHORIZED_401) // Halt with status
send(UNAUTHORIZED_401, "Go away!") // Halt with status and message
internalServerError("Body Message") // Halt with message (status 500)
internalServerError() // Halt with status 500
}
|
Redirects
You can redirect requests (returning 30x codes) by using Call
utility methods:
| get("/redirect") {
send(FOUND_302, "/call") // browser redirect to /call
}
|
Cookies
The request and response cookie functions provide a convenient way for sharing information between
handlers, requests, or even servers.
You can read client sent cookies from the request's cookies
read only map. To change cookies or
add new ones you have to use response.addCookie()
and response.removeCookie()
methods.
Check the following sample code for details:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | get("/cookie") {
request.cookies // Get map of all request cookies
request.cookiesMap()["foo"] // Access request cookie by name
val cookie = Cookie("new_foo", "bar")
ok(
cookies = listOf(
cookie, // Set cookie with a value
cookie.with(maxAge = 3600), // Set cookie with a max-age
cookie.with(secure = true), // Secure cookie
cookie.delete(), // Remove cookie
)
)
}
|
Error Handling
You can provide handlers for runtime errors. Errors are unhandled exceptions in the callbacks, or
handlers returning error codes.
Exception Mapping
You can handle previously thrown exceptions of a given type (or subtype). The handler allows you to
refer to the thrown exception. Look at the following code for a detailed example:
| // Register handler for routes which callbacks throw exceptions
get("/exceptions") { error("Message") }
get("/codedExceptions") { send(509, "code") }
before(pattern = "*", status = 509) {
send(599)
}
exception<IllegalStateException> {
send(HTTP_VERSION_NOT_SUPPORTED_505, exception?.message ?: "empty")
}
|
Static Files
You can use a [FileCallback] or a [UrlCallback] to route requests to files or classpath resources.
Those callbacks can point to folders or to files. If they point to a folder, the pattern should
have a parameter to provide the file to be fetched inside the folder.
Check the next example for details:
1
2
3
4
5
6
7
8
9
10
11
12 | get("/web/file.txt") { ok("It matches this route and won't search for the file") }
// Expose resources on the '/public' resource folder over the '/web' HTTP path
on(
status = NOT_FOUND_404,
pattern = "/web/*",
callback = UrlCallback(urlOf("classpath:public"))
)
// Maps resources on 'assets' on the server root (assets/f.css -> /f.css)
// '/public/css/style.css' resource would be: 'http://{host}:{port}/css/style.css'
on(status = NOT_FOUND_404, pattern = "/*", callback = UrlCallback(urlOf("classpath:assets")))
|
The media types of static files are computed from the file extension using the utility methods of
the com.hexagontk.core.media package.
WebSockets
A Web Socket is an HTTP(S) connection made with the GET method and the upgrade: websocket
and
connection: upgrade
headers.
If the server is handling HTTPS connections, the client should use
the WSS protocol.
When the server receives such a request, it is handled as any other route, and if everything is ok
the connection is converted to a permanent full duplex socket.
If the HTTP request is correct, callbacks should be supplied to handle WS events. Otherwise, if
standard HTTP errors are returned, the connection is closed. This gives the programmer an
opportunity to check the request format or authorization.
Once the WS session is created (after checking the upgrade request is correct), the upgrade request
data can be accessed inside the WS session.
Sessions connected to the same WS endpoint can be stored to broadcast messages.
Ping and pong allows to maintain connection opened.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 | private var sessions = emptyMap<Int, List<WsSession>>()
override val handler: HttpHandler = path {
ws("/ws/{id}") {
// Path parameters can also be accessed on WS handlers like `onText`
val idParameter = pathParameters.require("id")
// Request is handled like other HTTP methods, if errors, no WS connection is made
val id = idParameter.toIntOrNull()
?: return@ws badRequest("ID must be a number: $idParameter")
// Accepted returns the callbacks to handle WS requests
accepted(
// All callbacks have their session as the receiver
onConnect = {
val se = sessions[id] ?: emptyList()
sessions = sessions + (id to se + this)
},
onBinary = { bytes ->
if (bytes.isEmpty()) {
// The HTTP request data can be queried from the WS session
val certificateSubject = request.certificate()?.subjectX500Principal?.name
send(certificateSubject?.toByteArray() ?: byteArrayOf())
}
},
onText = { text ->
val se = sessions[id] ?: emptyList()
for (s in se)
// It is allowed to send data on previously stored sessions
s.send(text)
},
// Ping requests helps to maintain WS sessions opened
onPing = { bytes -> pong(bytes) },
// Pong handlers should be used to check sent pings
onPong = { bytes -> send(bytes) },
// Callback executed when WS sessions are closed (on the server or client side)
onClose = { status, reason ->
logger.info { "$status: $reason" }
val se = sessions[id] ?: emptyList()
sessions = sessions + (id to se - this)
}
)
}
}
|
Testing
Mocking Calls
To unit test callbacks and handlers you can create test calls with hardcoded requests without
relying on mocking libraries.
For a quick example, check the snipped below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | // Test callback (basically, a handler without a predicate)
val callback: HttpCallbackType = {
val fakeAttribute = attributes["fake"]
val fakeHeader = request.headers["fake"]?.value
ok("Callback result $fakeAttribute $fakeHeader")
}
// You can test callbacks with fake data
val resultContext = callback.process(
attributes = mapOf("fake" to "attribute"),
headers = Headers(Header("fake", "header"))
)
assertEquals("Callback result attribute header", resultContext.response.bodyString())
// Handlers can also be tested to check predicates along the callbacks
val handler = Get("/path", callback)
val notFound = handler.process(HttpRequest())
val ok = handler.process(HttpRequest(method = GET, path = "/path"))
assertEquals(NOT_FOUND_404, notFound.status)
assertEquals(OK_200, ok.status)
assertEquals("Callback result null null", ok.response.bodyString())
|
Package com.hexagontk.http.handlers
This package defines server interfaces for HTTP server adapters.