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

The hexagon_web module provides utilities on top of this port for Web application development (like templates helpers).

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:

  • serviceName: name of this service, it is only informative and it is displayed on the logs. If not set <undefined> is used.
  • bindAddress: address to which this process is bound. If none is provided, 127.0.0.1 is taken.
  • bindPort: the port that the process listens to. By default it is 2010
  • contextPath: initial path used for the rest of the routes, by default it is empty.

You can inject an adapter for the Server port using the InjectionManager object: InjectionManager.bindObject<ServerPort>(JettyServletAdapter())

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.

/*
 * All settings are optional, you can supply any combination
 * Parameters not set will fall back to the defaults
 */
val settings = ServerSettings(
    serverName = "name",
    bindAddress = InetAddress.getByName("0.0.0"),
    bindPort = 2020,
    contextPath = "/context"
)

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

val customServer = Server(adapter, router, settings)

customServer.start()

val customClient = Client("http://localhost:${customServer.runtimePort}")
assert(customServer.started())
assert(customClient.get("/context/hello").responseBody == "Hello World!")

customServer.stop()

/*
 * You can skip the adapter is you previously bound one
 * You may also skip the settings an the defaults will be used
 */
InjectionManager.bindObject(adapter)
val defaultServer = Server(router = router)

defaultServer.start()

val defaultClient = Client("http://localhost:${defaultServer.runtimePort}")
assert(defaultServer.started())
assert(defaultClient.get("/hello").responseBody == "Hello World!")

defaultServer.stop()
Servlet Web server

There is an 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.

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:

get("/hello") { ok("Get greeting")}
put("/hello") { ok("Put greeting")}
post("/hello") { ok("Post greeting")}

any("/hello") { ok("Fallback if HTTP verb was not used before")}

get { 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:

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:

fun personRouter(kind: String) = Router {
    get { ok("Get $kind") }
    put { ok("Put $kind") }
    post { ok("Post $kind") }
}

val server = Server(adapter) {
    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:

get("/call") {
    attributes                   // the attributes list
    attributes["foo"]            // value of foo attribute
    attributes["A"] = "V"        // sets value of attribute A to V

    ok("Response body")          // returns a 200 status
    send(400, "Invalid request") // returns any status
}
Request

Request functionality is provided by the request field:

get("/request") {
    request.method                   // the HTTP method (GET, ..etc)
    request.scheme                   // http or https
    request.secure                   // true if scheme is https
    request.host                     // the host, e.g. "example.com"
    request.ip                       // client IP address
    request.port                     // the server port
    request.path                     // the request path, e.g. /result.jsp
    request.body                     // request body sent by the client
    request.url                      // the url. e.g. "http://example.com/foo"
    request.contentLength            // length of request body
    request.contentType              // content type of request.body
    request.accept                   // Client accepted content types
    request.headers                  // the HTTP header list
    request.headers["BAR"]           // value of BAR header
    request.userAgent                // user agent (browser requests)
    request.origin                   // origin (browser requests)
    request.body(Type::class)        // Object passed in the body as a typed object
    request.body<Type>()             // Syntactic sugar for the previous statement
    request.bodyObjects(Type::class) // Object(s) passed in the body as a typed list
    request.bodyObjects<Type>()      // Syntactic sugar for the previous statement
    request.body(Map::class)         // Object passed in the body as a field map
    request.body<Map<*, *>>()        // Syntactic sugar for the previous statement
    request.bodyObjects(Map::class)  // Object(s) passed in the body as a list of maps
    request.bodyObjects<Map<*, *>>() // Syntactic sugar for the previous statement
}
Response

Response information is provided by the response field:

get("/response") {
    response.body                           // get response content
    response.body = "Hello"                 // sets content to Hello
    response.headers["FOO"] = listOf("bar") // sets header FOO with value bar
    response.status                         // get the response status
    response.status = 401                   // set status code to 401
    response.contentType                    // get the content type
    response.contentType = "text/xml"       // set content type to text/xml
}
Path Parameters

Route patterns can include named parameters, accessible via the pathParameters map on the request object:

get("/pathParam/{foo}") {
    request.pathParameters["foo"] // value of foo path parameter
    request.pathParameters        // map with all parameters
}
Query Parameters

It is possible to access the whole query string or only an specific query parameter using the parameters map on the request object:

get("/queryParam") {
    request.queryString
    request.queryParameters                 // the query param list
    request.queryParameters["FOO"]?.first() // value of FOO query param
    request.queryParameters["FOO"]          // all values of FOO query param
}
Redirects

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

get("/redirect") {
    redirect("/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:

get("/cookie") {
    request.cookies                       // get map of all request cookies
    request.cookies["foo"]                // access request cookie by name

    val cookie = HttpCookie("new_foo", "bar")
    response.addCookie(cookie)            // set cookie with a value

    cookie.maxAge = 3600
    response.addCookie(cookie)            // set cookie with a max-age

    cookie.secure = true
    response.addCookie(cookie)            // secure cookie

    response.removeCookie("foo")          // remove cookie
}
Sessions

Every request has access to the session created on the server side, the session object provides the following methods:

get("/session") {
    session                         // create and return session
    session.attributes["user"]      // Get session attribute 'user'
    session.set("user", "foo")      // Set session attribute 'user'
    session.removeAttribute("user") // Remove session attribute 'user'
    session.attributes              // Get all session attributes
    session.id                      // Get session id
    session.isNew()                 // Check if session is new
}
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:

get("/halt") {
    halt()                // halt with status 500 and stop route processing

    /*
     * These are just examples the following code will never be reached
     */
    halt(401)             // halt with status
    halt("Body Message")  // halt with message (status 500)
    halt(401, "Go away!") // halt with status and message
}

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). But 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:

before { response.headers["b_all"] = listOf("true") }

before("/filters/*") { response.headers["b_filters"] = listOf("true") }
get("/filters/route") { ok("filters route") }
after("/filters/*") { response.headers["a_filters"] = listOf("true") }

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

path("/nested") {
    before { response.headers["b_nested"] = listOf("true") }
    before("/") { response.headers["b_nested_2"] = listOf("true") }
    get("/filters") { ok("nested filters") }
    get("/halted") { halt(499, "halted") }
    get { ok("nested also") }
    after { response.headers["a_nested"] = listOf("true") }
}

after { response.headers["a_all"] = listOf("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 that will be used.

HTTP Errors Handlers

Allows to handle 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:

// Register handler for routes halted with 512 code
error(512) { send(500, "Ouch")}

// If status code (512) is returned with `send` error won't be triggered
get("/errors") { halt(512) }
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:

// Register handler for routes which callbacks throw an `IllegalStateException`
error(IllegalStateException::class) { send(505, it.message ?: "empty") }
get("/exceptions") { error("Message") }

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). And 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:

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
get("/web/*", Resource("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'
get(Resource("assets"))
MIME types

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

Testing

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 that you can check later:

val router = Router {
    get("/hello") { ok("Hi!") }
}

val serverSettings = ServerSettings("name", InetAddress.getLoopbackAddress(), 0)
val server = Server(adapter, router, serverSettings)

server.start()
val client = Client("http://localhost:${server.runtimePort}")
assert(client.get("/hello").responseBody == "Hi!")
server.stop()

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

Packages

Name Summary
com.hexagonkt.http.server This package defines the classes used in the HTTP DSL.

Index

All Types

Comments