Skip to content
Kona

Middleware

Middleware is code that runs around a handler. It sees the request before the handler does and the response after the handler returns, which makes it the right place for concerns shared by many routes: request IDs, authentication, access logs, timing, CORS headers, or forcing a response format.

A middleware is any object implementing the Middleware interface:

fun interface Middleware {
fun handle(request: Request, next: Handler): Response
}

next is “the rest of the pipeline” — the next middleware, or the route handler if this is the last one. A middleware does one of three things:

  1. Pass through: call next with the request and return its response, possibly after modifying it.
  2. Short-circuit: return a Response without calling next. Nothing further in the chain runs. This is how an authentication middleware rejects a request.
  3. Throw: raise an exception (for example HttpException(401, "Unauthenticated.")). The kernel’s exception handler turns it into a response; see Where exceptions go.

This middleware ensures every response carries an X-Request-Id header, reusing the client’s ID when it sent one and storing it in the request attributes so handlers can log it.

import org.konaframework.http.Handler
import org.konaframework.http.Middleware
import org.konaframework.http.Request
import org.konaframework.http.Response
import org.konaframework.http.invoke
import java.util.UUID
class RequestIdMiddleware : Middleware {
override fun handle(request: Request, next: Handler): Response {
val id = request.header("X-Request-Id") ?: UUID.randomUUID().toString()
request.setAttribute("requestId", id)
val response = next(request)
return response.header("X-Request-Id", id)
}
}

In Kotlin, next(request) is shorthand for next.handle(request); it needs import org.konaframework.http.invoke when your file is in another package. Both spellings do the same thing.

class Authenticate(private val tokens: TokenStore) : Middleware {
override fun handle(request: Request, next: Handler): Response {
val token = request.header("Authorization")?.removePrefix("Bearer ")
val user = token?.let(tokens::find)
?: return Response.json(mapOf("message" to "Unauthenticated."), 401)
request.setAttribute("user", user)
return next.handle(request)
}
}

Because Middleware is a functional interface, small middleware can be a lambda:

val poweredBy = Middleware { request, next -> next.handle(request).header("X-Powered-By", "Kona") }

Keep a reference to a lambda if you need to check for it later with hasMiddleware; two separately written lambdas are never equal.

  • Request: method, path, headers, query, and body are read-only. Use attributes (setAttribute / getAttribute) to pass data to later stages.
  • Response: status, body, and headers are mutable. Use header, withStatus, and withBody, or return a completely new Response.

Middleware objects are shared by all requests, which run concurrently on separate threads. Keep per-request data in local variables or request attributes, never in fields of the middleware.

Global middleware belongs to the HttpKernel and runs for every request — including requests that match no route. It wraps routing, route middleware, the handler, and the exception handling for all of those. As a result, global middleware sees the final 404, 405, 422, or 500 response just like a successful one, which is exactly what an access log or a security-header middleware needs.

Configure it on the kernel, usually from a service provider’s boot method:

override fun boot(app: Application) {
app.resolve(HttpKernel::class.java)
.pushMiddleware(RequestIdMiddleware())
.pushMiddleware(AccessLogMiddleware())
.prependMiddleware(SecurityHeaders())
}
Method Effect
pushMiddleware(mw) Add at the end of the list — the innermost position. Skipped if already present.
prependMiddleware(mw) Add at the front — the outermost position. Skipped if already present.
setGlobalMiddleware(list) Replace the list entirely (no duplicate check).
hasMiddleware(mw) Whether mw is in the list (compared with equals).
globalMiddleware / getGlobalMiddleware() The current list.

You can also pass the list when you construct a kernel yourself: HttpKernel(router, listOf(a, b)) in Kotlin, new HttpKernel(router, List.of(a, b)) in Java. Each request uses a snapshot of the list taken when it starts.

Route middleware runs only after a route has been selected, and only for that route. Attach it to one route with .middleware(...), or to many at once through a group:

router.get("/reports") { reports.index(it) }.middleware(Authenticate(tokens), RequireRole("analyst"))
router.group(prefix = "/admin", middleware = listOf(Authenticate(tokens), RequireAdmin())) {
get("/users") { admin.users(it) }
}

Route middleware does not run for 404 responses, 405 responses, or automatic OPTIONS replies, because no route was selected. For a HEAD request served by a GET route, the GET route’s middleware runs.

The list order is the wrapping order: the first middleware is the outermost layer. For global middleware [A, B], group middleware [G], and route middleware [R], a request flows like this:

A before → B before → (route resolved) → G before → R before → handler
A after ← B after ← G after ← R after ←

Code you write before next runs top-down; code after next runs in reverse. Nested groups append their middleware after the outer group’s, and .middleware(...) on the route itself comes last.

The kernel catches exceptions in two places, and which one applies changes who can see the error:

  • Thrown inside routing — by the router (404/405), by route or group middleware, or by the handler. The exception first propagates up through the route middleware, so a route middleware can try/catch around next if it wants to recover. If it is not caught, the kernel’s ExceptionHandler converts it to a response, and that response then passes back out through all global middleware normally.
  • Thrown by a global middleware itself. The kernel catches it at the very outside and converts it with the same exception handler. Global middleware does not see this response, because the chain has already unwound past them.

Global middleware therefore never receives an exception from next — it always receives a Response. See Exceptions for the response format and Kernel & server for the full pipeline.

Implement TerminableMiddleware when you need work after the response is fully produced (flush metrics, write access logs). The kernel calls terminate(request, response) for every terminable middleware that ran — global first, then route — in reverse order, after HEAD body stripping.

class AccessLog : TerminableMiddleware {
override fun handle(request: Request, next: Handler): Response = next(request)
override fun terminate(request: Request, response: Response) {
Log.info("${request.method} ${request.path} → ${response.status}")
}
}

HttpKernel.priority(PreferFirst.class, PreferSecond.class) reorders the global middleware list so listed types run earlier (outermost). Unlisted middleware keep their relative order after the prioritized ones. Route middleware is not reordered by priority.

MiddlewarePipeline.pipe(middlewares, destination) builds the same chain the kernel uses, which lets you test a middleware in isolation:

val handler = MiddlewarePipeline.pipe(listOf(RequestIdMiddleware())) { Response.text("ok") }
val response = handler.handle(Request.get("/"))
check(response.headers.containsKey("X-Request-Id"))

Apps from kona new configure middleware in bootstrap.Middleware.configure(app). That helper uses HttpMiddleware — a registrar for aliases, the web stack (pushed onto the global kernel), an optional api stack (for route groups), and optional trust middleware.

Typical scaffold (simplified):

object Middleware {
@JvmStatic
fun configure(app: Application) {
val sessions = app.resolve(SessionManager::class.java)
val files = app.resolve(FilesystemManager::class.java)
app.resolve(HttpKernel::class.java)
.prependMiddleware(ServePublicStorage(files.disk("public")))
HttpMiddleware()
.alias("start.session") { StartSession(sessions) }
.alias("csrf") { VerifyCsrfToken(except = listOf("/api/*", "/brew/*")) }
.web(listOf("start.session", "csrf"))
// .trustProxies(at = listOf("*"))
// .trustHosts(at = listOf("example.com", "*.example.com"))
.apply(app)
}
}
Method Role
alias(name, factory) Register a named middleware supplier
web(prepend, append) / api(...) Replace the named stack with alias names
webStack() / apiStack() Resolve aliases to middleware instances
trustProxies(at, headers) Prepend TrustProxies on apply
trustHosts(at) Prepend TrustHosts on apply
apply(app) Prepend trust middleware, push web stack onto HttpKernel, bind this registrar in the container

The api stack is not applied globally — resolve apiStack() and pass it to a route group when you want session-less JSON middleware of your own design.

Generate a stub with ./grind make:middleware Name.

Type Module Role
StartSession session Load/save session; flash; previous URL
VerifyCsrfToken session Reject forged form posts; except path patterns
ServePublicStorage http Serve public disk files under /storage/...
TrustProxies http Copy X-Forwarded-* / X-Real-IP onto request attributes when the peer is trusted
TrustHosts http Reject unknown Host headers with 400
ValidateSignature http Require a valid signed query string (URL generation)
CorsMiddleware auth/security CORS headers
SecurityHeadersMiddleware auth/security Security response headers

TrustProxies sets attributes such as kona.clientIp, kona.scheme, kona.host, kona.port. Request.ip() reads kona.clientIp then kona.remoteAddr. See Security guides for CSRF and session detail.

Type Role
Middleware handle(request, next): Response
TerminableMiddleware Plus terminate(request, response) after the response is produced
MiddlewarePipeline.pipe Build a chain for tests or custom dispatch
MiddlewarePipeline.sortByPriority Reorder by type list
HttpKernel push/prepend/set/priority Global stack control
HttpMiddleware Alias + web/api + trust bootstrap

Why doesn’t my route middleware run on 404?
No route was selected. Use global middleware for access logs that must see misses.

Why is my middleware running twice?
pushMiddleware skips duplicates by equals; lambdas are never equal to a previous lambda. Prefer named classes.

Can I attach middleware by string alias on a route?
Not today on Route.middleware(...) — pass instances. Aliases are for HttpMiddleware stacks.

Does method spoofing (_method) live here?
No. Crema can emit a hidden _method field in HTML, but the kernel does not rewrite the HTTP method from that field today — see Crema forms.

  • String middleware aliases on individual Route.middleware("auth") calls
  • Automatic _method override middleware
  • Middleware groups beyond what you build with HttpMiddleware + route group(...)
  • Coroutine / reactive middleware APIs (handlers are blocking on virtual threads)