Routing API (Advanced)
This is a fairly comprehensive example of the core-routing logic available:
- Individual HTTP endpoints are represented as
HttpHandlers. - Binding an
HttpHandlerto a path and HTTP verb yields aRoute. Routescan be combined together into aRoutingHttpHandler, which is both anHttpHandlerand aRouter.- A
Routeris a selective request handler, which attempts to match a request. If it cannot, processing falls through to the nextRouterin the list. - Routers can be combined together to form another
HttpHandler - Usage of supplied core library
Filters - Serving of static content using a
Classpathresource loader - Support for Single Page Applications using a
singlePageApp()block - resources loaded from here are loaded from the underlyingResourceLoaderor fallback to/(and passed to the SPA code)
Dynamic Paths / Path Variables
As you would expect, http4k allows routes to include dynamic or variable elements in the matching path, and allows you to reference the variable in the Handler. For example:
"/book/{title}" bind GET to { req ->
Response.invoke(Status.OK).body(GetBookDetails(req.path("title"))
}
"/author/{name}/latest" bind GET to { req ->
Response.invoke(Status.OK).body(GetAllBooks(author = req.path("name")).last())
}
By default, the variable(s) will match anything. However you can append the variable name with a RegEx expression to limit the matches.
// will match /book/978-3-16-148410-0 (i.e. only digits and dashes)
// /book/A%20Confederacy%20Of%20Dunces would return a 404 (Not Found)
"/book/{isbn:[\\d-]+}"
// will NOT match /sales/south or /sales/usa
"/sales/{region:(?:northeast|southeast|west|international)}"
There are no pre-defined types such as int or path for matching but these are easy to replicate with RegEx’s:
- string (excluding slashes) :
[^\\/]+(note that Kotlin requires backslashes to be escaped, so\win RegEx is expressed as\\win Kotlin) - int :
\\d+ - float :
[+-]?([0-9]*[.])?[0-9]+(this will match basic floats. Does not match exponents, or scientific notation) - path :
.*
Note that paths, not strings, will match by default. "/news/{date}" will match www.example.com/news/2018/05/26, making request.path("date") equal to 2018/05/26. This may be exactly what you want, or it may produce unexpected results, depending on how your URLs are structured.
Gradle setup
dependencies {
implementation(platform("org.http4k:http4k-bom:6.24.0.0"))
implementation("org.http4k:http4k-core")
}
Code
package content.howto.nestable_routes
import org.http4k.core.Method.DELETE
import org.http4k.core.Method.GET
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.filter.DebuggingFilters.PrintRequestAndResponse
import org.http4k.routing.ResourceLoader.Companion.Classpath
import org.http4k.routing.and
import org.http4k.routing.bind
import org.http4k.routing.header
import org.http4k.routing.headers
import org.http4k.routing.path
import org.http4k.routing.queries
import org.http4k.routing.routes
import org.http4k.routing.singlePageApp
import org.http4k.routing.static
fun main() {
val routesWithFilter =
PrintRequestAndResponse().then(
routes(
"/get/{name}" bind GET to { req: Request ->
Response(OK).body(req.path("name")!!)
},
"/post/{name}" bind POST to { Response(OK) }
)
)
println(routesWithFilter(Request(GET, "/get/value")))
val staticWithFilter = PrintRequestAndResponse()
.then(static(Classpath("guide/howto/nestable_routes")))
val app = routes(
"/bob" bind routesWithFilter,
"/static" bind staticWithFilter,
"/pattern/{rest:.*}" bind { req: Request ->
Response(OK).body(req.path("rest").orEmpty())
},
"/rita" bind routes(
"/delete/{name}" bind DELETE to { Response(OK) },
"/post/{name}" bind POST to { Response(OK) }
),
"/matching" bind GET to routes(
header("requiredheader", "somevalue")
.and(queries("requiredquery")) bind {
Response(OK).body("matched 2 parameters")
},
headers("requiredheader") bind { Response(OK).body("matched 1 parameters") }
),
singlePageApp(Classpath("guide/howto/nestable_routes"))
)
println(app(Request(GET, "/bob/get/value")))
println(app(Request(GET, "/static/someStaticFile.txt")))
println(app(Request(GET, "/pattern/some/entire/pattern/we/want/to/capture")))
println(
app(
Request(GET, "/matching")
.header("requiredheader", "somevalue")
.query("requiredquery", "somevalue")
)
)
println(app(Request(GET, "/matching").header("requiredheader", "somevalue")))
println(app(Request(GET, "/someSpaResource")))
}
For the typesafe contract-style routing, refer to this recipe instead.
Serving Static Content - Security Note
When using ResourceLoader.Classpath() to serve static resources in production, be aware that requests for non-existent files can cause unbounded growth in the JVM’s internal ClassLoader cache. This can be exploited to cause memory exhaustion. While it’s technically possible to implement workarounds such as pre-loading resources or custom caching strategies, the complexity and potential side effects make these approaches impractical for a general-purpose core library.
For production deployments, we recommend:
- Using
ResourceLoader.Directory()instead ofClasspath() - Deploying behind a reverse proxy (nginx, Apache, CDN) that handles static assets
- Implementing rate limiting or a WAF to protect against malicious request patterns
ResourceLoader.Classpath() is best suited for development and simple use cases where the application is not directly exposed to untrusted traffic.
