Skip to content

HTTP Server

Module http_server

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 transforming the DSL into a runtime. And allows you to switch implementations without changing the service.

Install the Dependency

This module is not meant to be used directly. You should include and Adapter implementing this feature (as http_server_jetty) in order to create an HTTP server.

Server

A server is a process listening to HTTP requests on a TCP port.

You can run multiple ones on different ports at the same time (this can be useful to test many microservices at the same time).

The server can be configured with different properties. If you do not provide a value for them, they are searched inside the application settings and lastly, a default value is picked. This is the parameters list:

  • banner: informative text shown at start up logs. If not set only runtime information is displayed.
  • bindAddress: address to which this process is bound. If none is provided, 127.0.0.1 is taken.
  • bindPort: the port which the process listens to. By default, it is 2010.
  • contextPath: initial path used for the rest of the routes, by default it is empty.

To create a server, you need to provide a router (check the next section for more information), and after creating a server you can run it or stop it with start() and stop() methods.

 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
/*
 * All settings are optional, you can supply any combination
 * Parameters not set will fall back to the defaults
 */
val settings = HttpServerSettings(
    bindAddress = InetAddress.getByName("0.0.0"),
    bindPort = 2020,
    contextPath = "/context",
    banner = "name"
)

val path = path {
    get("/hello") { ok("Hello World!") }
}

val runningServer = serve(serverAdapter(), listOf(path), settings)

// Servers implement closeable, you can use them inside a block assuring they will be closed
runningServer.use { s ->
    HttpClient(clientAdapter(), URL("http://localhost:${s.runtimePort}")).use {
        it.start()
        assert(s.started())
        assertEquals("Hello World!", it.get("/context/hello").body)
    }
}

/*
 * You may skip the settings and the defaults will be used
 */
val defaultSettingsServer = serve(serverAdapter(), listOf(path))
Servlet Web server

There is a special server adapter for running inside Servlet Containers. To use it you should import the Servlet HTTP Server Adapter into your project. Check the http_server_servlet module for more information.

Context on HTTP processing

An HTTP server is nothing more than a function that takes a request and returns a response. Requests and responses comply with several Web standards.

For the sake of ease of use, HTTP toolkits (or frameworks) are build. These tools make easier to write an HTTP server that has to deal with different behaviour based on requests attributes.

These development tools usually have different layers/parts: * IO * HTTP messages (requests and responses) parser and writer. * Routing

Hexagon takes care of the third layer (it's "just" an abstraction layer for the IO engine underneath), previous two layers depends on the adapter. This particularity allows users to swap adapters for different use cases. You can use a low memory for embedded boards (as Raspberry Pi) or high through-output for servers, another use case would be to use a fast boot adapter for development, and a different one for production. To be agnostic of the adapter below, a custom HTTP model is implemented and adapters must map their own structures to this model.

Most of the tools (in the JVM world) use metaprogramming (annotations) to effectively perform the HTTP parsing and request routing. However, this poses a problem. The code must run on a container and your handling code is called from generated code that is harder for you to tackle. This also makes harder to create many instances (something very useful to test microservices, for example).

On the other hand you have toolkits, toolkits are just a set of plain libraries that you call, you can build methods out of them (not easy to do for annotations).

In the past, metaprogramming would simplify development as Java lacked some language features (like lambdas) that made writing some HTTP routing constructions harder, but now I don't see any advantage (this is something similar to Dependency Injection, by the way)

For some advanced HTTP features (SSE and Websockets) asynchronous processing is desirable (as the connections are kept open more time and the 'one thread per request' model doesn't scale well on this basis). This is not yet implemented, but it will be.

Hexagon HTTP Handlers are a list of functions that may or may not be applied to the call (tuple of request and response) based on a filter (more details below).

Http Call (Event)

Handlers

Path Handler

Filters

Path Pattern
Supplying custom patterns using functions

Callbacks

Routes

The main building block of a Hexagon HTTP service is a set of routes. A route is made up of three simple pieces:

  • A verb (get, post, put, delete, head, trace, connect, options). It can also be any.
  • A path (/hello, /users/{name}). Paths must start with '/' and trailing slash is ignored.
  • A callback code block.

The callback has a void return type. You should use Call.send() to set the response which will be returned to the user.

Routes are matched in the order they are defined. The first route that matches the request is invoked, and the following ones are ignored.

Check the next snippet for usage examples:

1
2
3
4
5
6
7
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) { ok("Get at '/' if no route matched before") }

HTTP clients will be able to reuse the routes to create REST services clients.

Route groups

Routes can be nested by calling the path() method, which takes a String prefix and gives you a scope to declare routes and filters (or more nested paths). Ie:

1
2
3
4
5
6
7
8
9
path("/nested") {
    get("/hello") { ok("Greeting") }

    path("/secondLevel") {
        get("/hello") { ok("Second level greeting") }
    }

    get { ok("Get at '/nested'") }
}
Routers

If you have a lot of routes, it can be helpful to group them into routers. You can create routers to mount a group of routes in different paths (allowing you to reuse them). Check this snippet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun personRouter(kind: String) = path {
    get { ok("Get $kind") }
    put { ok("Put $kind") }
    post { ok("Post $kind") }
}

val server = HttpServer(serverAdapter()) {
    path("/clients", personRouter("client"))
    path("/customers", personRouter("customer"))
}

Callbacks

Callbacks are request's handling blocks that are bound to routes or filters. They make the request, response and session objects available to the handling code.

Call

The Call object provides you with everything you need to handle a http-request.

It contains the underlying request and response, and a bunch of utility methods to return results, read parameters or pass attributes among filters/routes.

The methods are available directly from the callback (Call is the callback receiver). 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
    send(
        BAD_REQUEST,
        "Invalid request",
        attributes = attributes + ("A" to "V") // sets value of attribute A to V
    )
}
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
47
get("/request") {
    // URL Information
    request.method                   // the HTTP method (GET, ..etc)
    request.protocol                 // http or https TODO
    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 header list with first values only
    request.headers["BAR"]           // first value of BAR header
    request.headers.allValues        // the HTTP header list with their full values list
    request.headers.allValues["BAR"] // list of values of BAR header

    // Common headers shortcuts
    request.contentType              // content type of request.body
    request.accept                   // Client accepted content types
    request.userAgent()              // user agent (browser requests)
    request.origin()                 // origin (browser requests)
    request.referer()                // TODO

    accept                             // Shortcut of `request.accept`

    // Parameters
    pathParameters                    // map with all path parameters
    request.formParameters            // map with first values of all form fields
    request.formParameters.allValues  // map with all form fields values
    request.queryParameters           // map with first values of all query parameters
    request.queryParameters.allValues // map with all query parameters values

    queryParameters                   // Shortcut of `request.queryParameters`
    queryParameters.allValues         // Shortcut of `request.queryParameters.allValues`
    formParameters                    // Shortcut of `request.formParameters`
    formParameters.allValues          // Shortcut of `request.formParameters.allValues`

    // 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:

1
2
3
4
5
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:

1
2
3
4
5
6
7
8
get("/queryParam") {
    request.queryParameters                       // the query param list
    request.queryParameters["FOO"]                // value of FOO query param
    request.queryParameters.allValues             // the query param list
    request.queryParameters.allValues["FOO"]      // all values of FOO query param

    ok()
}
Form Parameters

HTML Form processing. Don't parse body!

1
2
3
4
5
6
7
get("/formParam") {
    request.formParameters                       // the query param list
    request.formParameters["FOO"]                // value of FOO query param
    request.formParameters.allValues             // the query param list
    request.formParameters.allValues["FOO"]      // all values of FOO query param
    ok()
}
File Uploads

Multipart Requests

1
2
3
4
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
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,           // set status code to 401
        body = "Hello",                  // sets content to Hello
        contentType = ContentType(XML),  // set content type to application/xml
        headers = response.headers
            + Header("foo", "bar")           // sets header FOO with single value bar
            + HttpFields(Header("baz", "1", "2")) // sets header FOO values with [ bar ]
    )
}
Redirects

You can redirect requests (returning 30x codes) by using Call utility methods:

1
2
3
get("/redirect") {
    redirect(FOUND, "/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 = HttpCookie("new_foo", "bar")
    ok(
        cookies = listOf(
            cookie,                     // set cookie with a value
            cookie.copy(maxAge = 3600), // set cookie with a max-age
            cookie.copy(secure = true), // secure cookie
            cookie.delete(),            // remove cookie
        )
    )
}
Compression
Halting

To immediately stop a request within a filter or route use halt(). halt() is not intended to be used inside exception-mappers. Check the following snippet for an example:

1
2
3
4
5
6
get("/halt") {
    clientError(UNAUTHORIZED)             // halt with status
    clientError(UNAUTHORIZED, "Go away!") // halt with status and message
    internalServerError("Body Message")   // halt with message (status 500)
    internalServerError()                 // halt with status 500
}

Filters

You might know filters as interceptors, or middleware from other libraries. Filters are blocks of code executed before or after one or more routes. They can read the request and read/modify the response.

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.

Before and after filters are always executed (if the route is matched). However, any of them may stop the execution chain if halted.

If halt() is called in one filter, filter processing is stopped for that kind of filter (before or after). In the case of before filters, this also prevent the route from being executed (but after filters are executed anyway).

The following code details filters usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
on("/*") { send(headers = response.headers + Header("b-all", "true")) }

on("/filters/*") { send(headers = response.headers + Header("b-filters", "true")) }
get("/filters/route") { ok("filters route") }
after("/filters/*") { send(headers = response.headers + Header("a-filters", "true")) }

get("/filters") { ok("filters") }

path("/nested") {
    on("*") { send(headers = response.headers + Header("b-nested", "true")) }
    on { send(headers = response.headers + Header("b-nested-2", "true")) }
    get("/filters") { ok("nested filters") }
    get("/halted") { send(HttpStatus(499), "halted") }
    get { ok("nested also") }
    after("*") { send(headers = response.headers + Header("a-nested", "true")) }
}

after("/*") { send(headers = response.headers + Header("a-all", "true")) }

Error Handling

You can provide handlers for runtime errors. Errors are unhandled thrown exceptions in the callbacks, or handlers halted with an error code.

Error handlers for a given code or exception are unique, and the first one defined is the one which will be used.

HTTP Errors Handlers

Allows handling routes halted with a given code. These handlers are only applied if the route is halted, if the error code is returned with send it won't be handled as an error. Example:

1
2
3
4
5
6
7
8
exception<Exception>(NOT_FOUND) {
    internalServerError("Root handler")
}

// Register handler for routes halted with 512 code
get("/errors") { send(HttpStatus(512)) }

on(pattern = "*", status = HttpStatus(512)) { send(INTERNAL_SERVER_ERROR, "Ouch") }
Exception Mapping

You can handle exceptions of a given type for all routes and filters. The handler allows you to refer to the thrown exception. Look at the following code for a detailed example:

TODO add exceptionHandler usage and example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Register handler for routes which callbacks throw exceptions
get("/exceptions") { error("Message") }
get("/codedExceptions") { send(HttpStatus(509), "code") }

on(pattern = "*", status = HttpStatus(509)) {
    send(HttpStatus(599))
}
on(pattern = "*", exception = IllegalStateException::class) {
    send(HTTP_VERSION_NOT_SUPPORTED, exception?.message ?: "empty")
}

Static Files

You can use a folder in the classpath for serving static files with the get() methods. Note that the public directory name is not included in the URL.

Asset mapping is handled like any other route, so if an asset mapping is matched, no other route will be checked (assets or other routes). Also, if a previous route is matched, the asset mapping will never be checked.

Being get(resource) a shortcut of get("/*", resource) it should be placed as the last route. 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,
    pattern = "/web/*",
    callback = UrlCallback(URL("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, pattern = "/*", callback = UrlCallback(URL("classpath:assets")))
MIME types

The MIME types of static files are computed from the file extension using the SerializationManager.contentTypeOf() method.

CORS

CORS behaviour can be different depending on the path. You can attach different CorsSettings to different routers. Check CorsSettings class for more details.

 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
val path: PathHandler = path {
    corsPath("/default", CorsCallback())
    corsPath("/example/org", CorsCallback("example.org"))
    corsPath("/no/credentials", CorsCallback(supportCredentials = false))
    corsPath("/only/post", CorsCallback(allowedMethods = setOf(POST)))
    corsPath("/cache", CorsCallback(preFlightMaxAge = 10))
    corsPath("/exposed/headers", CorsCallback(exposedHeaders = setOf("head")))
    corsPath("/allowed/headers", CorsCallback(allowedHeaders = setOf("head")))
}

private fun ServerBuilder.corsPath(path: String, cors: CorsCallback) {
    path(path) {
        // CORS settings can change for different routes
        filter(pattern = "*", callback = cors)

        get("/path") { ok(method.toString()) }
        post("/path") { ok(method.toString()) }
        put("/path") { ok(method.toString()) }
        delete("/path") { ok(method.toString()) }
        get { ok(method.toString()) }
        post { ok(method.toString()) }
        put { ok(method.toString()) }
        delete { ok(method.toString()) }
    }
}

HTTPS

It is possible to start a secure server enabling HTTPS. For this, you have to provide a server certificate and its key in the server's SslSettings. Once you use a server certificate, it is also possible to serve content using HTTP/2, for this to work, ALPN is required (however, this is already handled if you use Java 11).

The certificate common name should match the host that will serve the content in order to be accepted by an HTTP client without a security error. There is a Gradle helper to create sample certificates for development purposes.

HTTP clients can also be configured to use a certificate. This is required to implement a double ended authorization (mutual TLS). This is also done by passing a SslSettings object the HTTP client.

If you want to implement mutual trust, you must enforce client certificate in the server configuration (check SslSettings.clientAuth). If this is done, you can access the certificate the client used to connect (assuming it is valid, if not the connection will end with an error) with the Request.certificateChain property.

Below you can find a simple example to set up an HTTPS server and client with mutual TLS:

 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
// Key store files
val identity = "hexagonkt.p12"
val trust = "trust.p12"

// Default passwords are file name reversed
val keyStorePassword = identity.reversed()
val trustStorePassword = trust.reversed()

// Key stores can be set as URIs to classpath resources (the triple slash is needed)
val keyStore = URL("classpath:ssl/$identity")
val trustStore = URL("classpath:ssl/$trust")

val sslSettings = SslSettings(
    keyStore = keyStore,
    keyStorePassword = keyStorePassword,
    trustStore = trustStore,
    trustStorePassword = trustStorePassword,
    clientAuth = true // Requires a valid certificate from the client (mutual TLS)
)

val serverSettings = HttpServerSettings(
    bindPort = 0,
    protocol = HTTPS, // You can also use HTTP2
    sslSettings = sslSettings
)

val server = HttpServer(serverAdapter(), serverSettings) {
    get("/hello") {
        // We can access the certificate used by the client from the request
        val subjectDn = request.certificate()?.subjectX500Principal?.name ?: ""
        val h = response.headers + Header("cert", subjectDn)
        ok("Hello World!", headers = h)
    }
}
server.start()

// We'll use the same certificate for the client (in a real scenario it would be different)
val clientSettings = HttpClientSettings(sslSettings = sslSettings)

// Create an HTTP client and make an HTTPS request
val contextPath = URL("https://localhost:${server.runtimePort}")
val client = HttpClient(clientAdapter(), contextPath, clientSettings)
client.start()
client.get("/hello").apply {
    // Assure the certificate received (and returned) by the server is correct
    assert(headers.require("cert").startsWith("CN=hexagonkt.com"))
    assertEquals("Hello World!", body)
}

Testing

Integration tests

To test HTTP servers from outside using a real Adapter, you can create a server setting 0 as port. This will pick a random free port which you can check later:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
val router = path {
    get("/hello") { ok("Hi!") }
}

val bindAddress = InetAddress.getLoopbackAddress()
val serverSettings = HttpServerSettings(bindAddress, 0, banner = "name")
val server = serve(serverAdapter(), listOf(router), serverSettings)

server.use { s ->
    HttpClient(clientAdapter(), URL("http://localhost:${s.runtimePort}")).use {
        it.start()
        assertEquals("Hi!", it.get("/hello").body)
    }
}

To do this kind of tests without creating a custom server (using the real production code). Check the tests of the starter projects.

If you have an OpenAPI/Swagger spec defined for your server, you can also make use of the mock server (see below).

Mocking calls

To unit test handlers you can call them with hardcoded requests.

For a quick sample, check the snipped below:

TODO Add example code

Package com.hexagonkt.http.server

This package defines the classes used in the HTTP DSL.

Package com.hexagonkt.http.server.callbacks

TODO

Package com.hexagonkt.http.server.handlers

TODO

Package com.hexagonkt.http.server.model

TODO