- Basic concepts
- Capturing path parameters
- Routing by wildcard
- Routing by regular expressions
- Routing by HTTP method
- Routing by content type
- Routing based on MIME types acceptable by the client
- Error handling
- Serving static resources
- Multipart file uploading
- CORS handler
Basic concepts
The router is one of the core concepts of Firefly HTTP server. It maintains zero or more Routes.
A router takes an HTTP request and finds the first matching route for that request, and passes the request to that route.
The route can have a handler associated with it, which then receives the request. You then do something with the request, and then, either end it or pass it to the next matching handler.
Here’s a simple router example:
fun main() {
`$`.httpServer()
.router().get("/").handler { ctx -> ctx.write("Hello world! ").next() } // (1)
.router().get("/").handler { ctx -> ctx.end("The router demo.") } // (2)
.listen("localhost", 8090)
}
Two routers handle GET method and path ‘/’. When you visit the http://localhost:8090
, the server response:
Hello world! The router demo.
Class $
The class $ provides primary API of Firefly, such as
- The HTTP server and client
- The WebSocket server and client
- The TCP server and client
- The UDP server and client
- Other utilities
It uses fluent style API to help you to build a complex application. Chaining calls like this allow you to write code that’s a little bit less verbose.
Route order
By default, routes are matched in the order which they are added to the router manager. Invokes the method $.httpServer().router()
to create a router.
When a request arrives, the router will step through each route and check if it matches then the handler for that route will be called.
Calling the next handler
If the handler subsequently calls ctx.next()
method the handler for the next matching route (if any) will be called. And so on.
In the first example, When the server receives the request, it invokes the first handler.
- The first handler writes the “Hello world!” and calls
ctx.next()
method to invoke the next router. - The second handler writes the “The router demo.” and ends the chain of responsibility. Then the server will flush the data to the client.
Routing Context
A new RoutingContext(ctx) instance is created for each HTTP request.
You can visit the RoutingContext instance in the whole router chain. It provides HTTP request/response API and you can save data in the routing context.
Here’s an example where one handler saves data in the routing context, and a subsequent handler reads it:
fun main() {
`$`.httpServer()
.router().get("/").handler { ctx ->
ctx.attributes["router1"] = "Some one visits the /. " // (1)
ctx.write("Hello world! ").next()
}
.router().get("/").handler { ctx ->
val data = ctx.attributes["router1"] // (2)
ctx.end("The router data: $data")
}
.listen("localhost", 8090)
}
You visit the http://localhost:8090
, the server response:
Hello world! The router data: Some one visits the /.
- The first handler saves data using
ctx.attributes
method. - The second handler reads data and flushes it to the client.
Capturing path parameters
It’s possible to match paths using placeholders for parameters. The placeholders consist of : followed by the parameter name. Parameter names consist of any alphabetic character, numeric character or underscore.
fun main() {
`$`.httpServer()
.router().get("/product/:id").handler { ctx ->
when (val id = ctx.getPathParameter("id")) {
"1" -> ctx.end("Apple")
"2" -> ctx.end("Orange")
else -> ctx.setStatus(NOT_FOUND_404).end("The product $id not found.")
}
}
.listen("localhost", 8090)
val url = "http://localhost:8090"
`$`.httpClient().get("$url/product/1").submit()
.thenAccept { response -> println(response.stringBody) } // (1)
`$`.httpClient().get("$url/product/2").submit()
.thenAccept { response -> println(response.stringBody) } // (2)
`$`.httpClient().get("$url/product/3").submit()
.thenAccept { response -> println(response.stringBody) } // (3)
}
In this case, we use the HTTP client to call the HTTP server.
- The
$.httpClient().get
builds the GET request and then submits it to the server, the server usesctx.getPathParameter(name: String)
method to get product id in the path. The server finds the apple and responseApple
. - The server gets the orange and respone
Orange
- The server can not find the product 3, so response
The product 3 not found.
Routing by wildcard
Often you want to route all requests that accord with a pattern. A simply way is to use wildcard *
. For example:
fun main() {
`$`.httpServer()
.router().put("/product/*/*").handler { ctx ->
val type = ctx.getPathParameter(0)
val id = ctx.getPathParameter(1)
val product = ctx.stringBody
ctx.end("Put product success. id: $id, type: $type, product: $product")
}
.listen("localhost", 8090)
val url = "http://localhost:8090"
`$`.httpClient().put("$url/product/fruit/1").body("Apple").submit()
.thenAccept { response -> println(response.stringBody) } // (1)
`$`.httpClient().put("$url/product/book/1").body("Tom and Jerry").submit()
.thenAccept { response -> println(response.stringBody) } // (2)
`$`.httpClient().put("$url/product/book/2").body("The Three-Body Problem").submit()
.thenAccept { response -> println(response.stringBody) } // (3)
}
The server uses ctx.getPathParameter(index: Int)
method to get path parameter. The index starts from 0.
- The
$.httpClient().put
builds the PUT request and then submits it to the server. The server responsePut product success. id: 1, type: fruit, product: Apple
. - The server response
Put product success. id: 1, type: book, product: Tom and Jerry
. - The server response
Put product success. id: 2, type: book, product: The Three-Body Problem
.
Routing by regular expressions
Regular expressions can also be used to match URI paths in routes. For example:
fun main() {
`$`.httpServer()
.router().method(HttpMethod.PUT).pathRegex("/product/(.*)/(.*)").handler { ctx ->
val type = ctx.getPathParameterByRegexGroup(1)
val id = ctx.getPathParameterByRegexGroup(2)
val product = ctx.stringBody
ctx.end("Put product success. id: $id, type: $type, product: $product")
}
.listen("localhost", 8090)
val url = "http://localhost:8090"
`$`.httpClient().put("$url/product/fruit/1").body("Apple").submit()
.thenAccept { response -> println(response.stringBody) }
`$`.httpClient().put("$url/product/book/1").body("Tom and Jerry").submit()
.thenAccept { response -> println(response.stringBody) }
`$`.httpClient().put("$url/product/book/2").body("The Three-Body Problem").submit()
.thenAccept { response -> println(response.stringBody) }
}
We use the getPathParameterByRegexGroup
method to get the path parameter. The regex group index starts from 1.
Routing by HTTP method
By the default, a route will match all HTTP methods if you do not call router.method
, router.get
, router.post
, router.put
or router.delete
. For example:
fun main() {
`$`.httpServer()
.router().get("/product/:id").handler { ctx ->
val id = ctx.getPathParameter("id")
ctx.end("Get the product $id")
}
.router().post("/product").handler { ctx ->
ctx.end("Create the product 1")
}
.router().put("/product/:id").handler { ctx ->
val id = ctx.getPathParameter("id")
ctx.end("Update the product $id")
}
.router().delete("/product/:id").handler { ctx ->
val id = ctx.getPathParameter("id")
ctx.end("Delete the product $id")
}
.listen("localhost", 8090)
}
In the above example, we build the RESTful APIs. The URL /product/:id
represents resources. The HTTP verbs (Such as, GET
, POST
, PUT
, DELETE
and so on) represent the operation of resources (Such as get, create, update and delete).
If you want to let a lot of HTTP methods match a router, just use the router.methods(list: List)
instead of router.method(name: String)
.
Routing by content type
You can specify that a route will match against matching request MIME types using method router.consumes
.
In this case, the request will contain a content-type header specifying the MIME type of the request body. This will be matched against the value specified in consumes.
Basically, The router.consumes
method is describing which MIME types the handler can consume. For example:
@NoArg
data class Car(var name: String, var color: String)
fun main() {
`$`.httpServer()
.router().put("/product/:id").consumes("*/json")
.handler { ctx ->
val id = ctx.getPathParameter("id")
val type = ctx.getPathParameter(0)
val car = json.read<Car>(ctx.stringBody)
ctx.write("Update product. id: $id, type: $type. \r\n")
.end(car.toString())
}
.listen("localhost", 8090)
val url = "http://localhost:8090"
`$`.httpClient()
.put("$url/product/3")
.add(HttpField(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.value))
.body(json.write(Car("Benz", "Black")))
.submit().thenAccept { response -> println(response.stringBody) }
}
Run it. The console shows:
Update product. id: 3, type: application.
Car(name=Benz, color=Black)
In the above example, we use the wildcard *
to match the content type of the HTTP request. We can also use the exact MIME type to match the request.
Routing based on MIME types acceptable by the client
The HTTP Accept header is used to signify which MIME types of the response are acceptable to the client.
An accept header can have multiple MIME types separated by ‘,’.
MIME types can also have a q value appended to them which signifies a weighting to apply if more than one response MIME type is available matching the HTTP Accept header. The q value is a number between 0 and 1.0. If omitted it defaults to 1.0.
For example, the following accept header signifies the client will accept a MIME type of only text/plain:
Accept: text/plain
With the following, the client will accept text/plain or text/html with no preference.
Accept: text/plain, text/html
With the following the client will accept text/plain or text/html but prefers text/html as it has a higher q value (the default value is q=1.0)
Accept: text/plain; q=0.9, text/html
If the server can provide both text/plain and text/html it should provide the text/html in this case.
By using router.produces
you define which MIME type(s) the route produces, e.g. the following handler produces a response with MIME type application/json.
fun main() {
`$`.httpServer()
.router().get("/product/:id").produces("text/plain")
.handler { ctx ->
ctx.end(Car("Benz", "Black").toString())
}
.router().get("/product/:id").produces("application/json")
.handler { ctx ->
ctx.put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.value)
.end(json.write(Car("Benz", "Black")))
}
.listen("localhost", 8090)
val url = "http://localhost:8090"
`$`.httpClient().get("$url/product/3")
.put(HttpHeader.ACCEPT, "text/plain, application/json;q=0.9, */*;q=0.8")
.submit().thenAccept { response -> println("accept text; ${response.stringBody}") }
`$`.httpClient().get("$url/product/3")
.put(HttpHeader.ACCEPT, "application/json, text/plain, */*;q=0.8")
.submit().thenAccept { response -> println("accept json; ${response.stringBody}") }
}
Run it. The console shows:
accept text; Car(name=Benz, color=Black)
accept json; {"name":"Benz","color":"Black"}
In the above example, the first request, the text/plain
weight(1.0) is higher than application/json
(0.9), so this request matches the first router that responds the text format.
The second request, the application/json
weight equals the text/plain
, but application/json
is in front of text/plain
, so the application/json
priority is higher than text/plain
. It matches the second router that responds the JSON format.
Error handling
When the handler throws exception, the server uses DefaultContentProvider
response the error message.
fun main() {
`$`.httpServer()
.router().post("/product").handler {
throw IllegalStateException("Create product exception") // (1)
}
.listen("localhost", 8090)
val url = "http://localhost:8090"
`$`.httpClient().post("$url/product/").submit()
.thenAccept { response -> println(response) }
}
The client will receive the Create product exception
.
Custom error handling
You can use httpServer.onException
method to output custom error message.
For example.
fun main() {
`$`.httpServer()
.onException { ctx, exception -> // (1)
ctx.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500)
.end("The server exception. ${exception.message}")
}
.router().post("/product").handler {
throw IllegalStateException("Create product exception")
}
.listen("localhost", 8090)
val url = "http://localhost:8090"
`$`.httpClient().post("$url/product/").submit()
.thenAccept { response -> println(response) }
}
The client will receive the The server exception. Create product exception
.
Serving static resources
Firefly comes with an out of the box handler for serving static web resources so you can write static web servers very easily.
To serve static resources such as .html, .css, .js or any other static resource, you use an instance of FileHandler
.
fun main() {
`$`.httpServer()
.router().method(HttpMethod.GET)
.paths(listOf("/favicon.ico", "/poem.html", "/poem.txt")) // (1)
.handler(FileHandler.createFileHandlerByResourcePath("files")) // (2)
.listen("localhost", 8090)
}
- Use the
router.paths
method to bind some path to the file handler. - Use the factory method
FileHandler.createFileHandlerByResourcePath
to create a file handler instance. When you visit thehttp://localhost:8090/poem.html
, the server will read the fileresources/files/poem.html
and flush the file to the client.
Multipart file uploading
@NoArg
data class Product(var id: String, var brand: String, var description: String)
fun main() {
`$`.httpServer()
.router().post("/product/file-upload").handler { ctx ->
val id = ctx.getPart("id") // (1)
val brand = ctx.getPart("brand")
val description = ctx.getPart("description")
ctx.end(Product(id.stringBody, brand.stringBody, description.stringBody).toString())
}
.listen("localhost", 8090)
val url = "http://localhost:8090"
`$`.httpClient().post("$url/product/file-upload")
.addPart("id", stringBody("x01"), null) // (2)
.addPart("brand", stringBody("Test"), null)
.addFilePart(
"description", "poem.txt",
resourceFileBody("files/poem.txt", StandardOpenOption.READ), // (3)
null
)
.submit().thenAccept { response -> println(response) }
}
- The server uses the
ctx.getPart
to get the content of multi-part format. - The client uses the
httpclient.addPart
to upload content. - The client uses the
httpclient.addFilePart
to upload file. The factory methodresourceFileBody
reads the resource file and encodes it to the multi-part format.
CORS handler
Cross Origin Resource Sharing is a safe mechanism for allowing resources to be requested from one domain and served from another.
The example:
fun main() {
val corsConfig = CorsConfig("*.cors.test.com")
`$`.httpServer()
.router().path("*").handler(CorsHandler(corsConfig)) // (1)
.router().post("/cors-data-request/*")
.handler { it.end("success") }
.listen("localhost", 8090)
val url = "http://localhost:8090"
`$`.httpClient().post("$url/cors-data-request/xxx")
.put(HttpHeader.ORIGIN, "hello.cors.test.com") // (2)
.put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.TEXT_PLAIN_UTF_8.value)
.body("hello")
.submit().thenAccept { response -> println(response) }
}
- The server uses the
CorsHandler
to set some origin can visit the server resources. - The client set the origin header, if the server allows this origin, the client can visit the server resources.